Skip to content

Add AI coding tool hooks integration (Claude Code, Codex, Cursor)#257

Open
AnnatarHe wants to merge 2 commits intomainfrom
claude/hooks-based-tracking-LqOJQ
Open

Add AI coding tool hooks integration (Claude Code, Codex, Cursor)#257
AnnatarHe wants to merge 2 commits intomainfrom
claude/hooks-based-tracking-LqOJQ

Conversation

@AnnatarHe
Copy link
Copy Markdown
Contributor

@AnnatarHe AnnatarHe commented Mar 12, 2026

Summary

This PR adds comprehensive support for tracking AI coding tool hook events from Claude Code, Codex, and Cursor. It includes installation/uninstallation commands, event processing infrastructure, and backend integration.

Key Changes

New Commands

  • aicode-hooks command: Main command to track hook events from AI coding tools

    • Reads JSON payloads from stdin
    • Auto-detects source tool (Claude Code, Codex, Cursor)
    • Sends events to daemon socket or directly via HTTP
    • Includes install and uninstall subcommands for hook setup
  • aicode-hooks install/uninstall subcommands: Manage hook registration

    • Claude Code: Modifies ~/.claude/settings.json to register hooks for 10 events (SessionStart, UserPromptSubmit, PostToolUse, etc.)
    • Codex/Cursor: Creates/updates ~/.codex/hooks.json and ~/.cursor/hooks.json
    • Supports selective installation per tool or all at once
    • Idempotent operations (checks for existing hooks before adding)

Daemon-Side Processing

  • AICodeHooksProcessor: New daemon component for batching and forwarding events
    • Subscribes to aicode_hooks topic via pub/sub
    • Batches events (configurable batch size and interval)
    • Sends batches to backend API endpoint
    • Supports debug mode for writing raw events to temp files
    • Graceful shutdown with event flushing

Data Models

  • AICodeHooksEventData: Comprehensive event structure capturing:

    • Event metadata (ID, timestamp, client type, session ID)
    • Tool execution details (tool name, input, response, use ID)
    • Session context (working directory, model, permission mode)
    • Hook-specific fields (notifications, agent info, transcript path)
    • Raw payload for extensibility
  • AICodeHooksRequest/Response: API contract for backend communication

Socket Integration

  • Added SocketMessageTypeAICodeHooks message type
  • New SendAICodeHooksToSocket() function for fire-and-forget event delivery
  • Socket handler routes aicode_hooks messages to pub/sub topic

Configuration

  • New AICodeHooks config struct with:
    • Enabled flag
    • BatchSize (default: 50)
    • BatchIntervalSeconds (default: 5)
    • Debug flag for troubleshooting
  • Config merging and initialization with sensible defaults

API Integration

  • SendAICodeHooksData(): HTTP POST to /api/v1/cc/aicode-hooks endpoint
  • Supports both daemon-mediated and direct HTTP delivery paths

Implementation Details

  • Events are parsed from raw JSON payloads with flexible field extraction
  • Source auto-detection based on payload structure (hook_event_name field)
  • Fallback to direct HTTP if daemon socket unavailable
  • Debug mode writes timestamped JSON events to /tmp/shelltime/aicode-hooks-debug-events.txt
  • Proper resource cleanup with timer management and graceful shutdown

https://claude.ai/code/session_01D711AM5UncwjVcqrov7YMR


Open with Devin

claude added 2 commits March 12, 2026 12:01
Add new `shelltime aicode-hooks` CLI command and daemon processor for
hooks-based tracking of AI coding tools (Claude Code, Codex, Cursor).

- New CLI command reads hook JSON from stdin, enriches with metadata
- Socket message type for AICodeHooks with pub/sub topic
- Daemon processor with batching (50 events / 5s window)
- Install/uninstall commands for hook configuration
- API client for POST /api/v1/cc/aicode-hooks
- Config support for aiCodeHooks section

https://claude.ai/code/session_01D711AM5UncwjVcqrov7YMR
…yload

Add NotificationType, NotificationMessage, and SessionEndReason fields
to match the server schema, and extract them from raw hook JSON.

https://claude.ai/code/session_01D711AM5UncwjVcqrov7YMR
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a robust system for tracking events from various AI coding tools like Claude Code, Codex, and Cursor. It provides a new command-line interface for managing tool-specific hooks, a daemon-side processor for efficient event handling and batching, and comprehensive data models and API integration to ensure these events are reliably sent to the backend for analysis. The changes aim to enhance observability into AI-assisted development workflows.

Highlights

  • New aicode-hooks Command: Added a new CLI command aicode-hooks with install and uninstall subcommands to manage AI coding tool hook events for Claude Code, Codex, and Cursor.
  • Daemon-Side Processing: Implemented AICodeHooksProcessor in the daemon to subscribe to, batch, and forward AI coding tool events to the backend API, including a debug mode for logging raw events.
  • Data Models and API Integration: Defined new data structures (AICodeHooksEventData, AICodeHooksRequest, AICodeHooksResponse) and an API function (SendAICodeHooksData) for structured event tracking and communication with the backend.
  • Socket Communication: Integrated a new SocketMessageTypeAICodeHooks and a corresponding function (SendAICodeHooksToSocket) for fire-and-forget event delivery to the daemon via Unix sockets.
  • Configuration: Introduced a new AICodeHooks configuration struct within ShellTimeConfig to control the feature's enablement, batch size, batch interval, and debugging options.
Changelog
  • cmd/cli/main.go
    • Added the new AICodeHooksCommand to the main CLI application.
  • cmd/daemon/main.go
    • Initialized and started the AICodeHooksProcessor within the daemon if its configuration is enabled.
  • commands/aicode_hooks.go
    • Created the aicode-hooks command, which processes JSON payloads from stdin, auto-detects the AI tool source, and dispatches event data to either the daemon socket or directly to the backend API.
  • commands/aicode_hooks_install.go
    • Implemented install and uninstall subcommands for aicode-hooks, managing the registration and removal of hooks in configuration files for Claude Code, Codex, and Cursor.
  • daemon/aicode_hooks_processor.go
    • Added the AICodeHooksProcessor to subscribe to AI code hook events, batch them, and send them to the backend API, including debug logging capabilities.
  • daemon/client.go
    • Introduced SendAICodeHooksToSocket to facilitate sending AI code hook event data to the daemon via a Unix socket.
  • daemon/socket.go
    • Defined SocketMessageTypeAICodeHooks and updated the socket handler to publish incoming AI code hook messages to the internal pub/sub topic.
  • model/aicode_hooks_types.go
    • Defined new Go structs (AICodeHooksRequest, AICodeHooksEventData, AICodeHooksResponse) and constants for AI code hook event data and client types.
  • model/api_aicode_hooks.go
    • Added SendAICodeHooksData to handle HTTP POST requests for sending batched AI code hook events to the configured API endpoint.
  • model/config.go
    • Updated configuration merging logic to include AICodeHooks settings and applied default values for batching parameters during configuration loading.
  • model/types.go
    • Added the AICodeHooks struct to the ShellTimeConfig to enable configuration of AI code hook tracking features.
Activity
  • No human activity (comments, reviews, etc.) has been recorded for this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 12, 2026

Codecov Report

❌ Patch coverage is 0% with 465 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
commands/aicode_hooks_install.go 0.00% 183 Missing ⚠️
daemon/aicode_hooks_processor.go 0.00% 131 Missing ⚠️
commands/aicode_hooks.go 0.00% 105 Missing ⚠️
model/api_aicode_hooks.go 0.00% 16 Missing ⚠️
daemon/client.go 0.00% 9 Missing ⚠️
daemon/socket.go 0.00% 8 Missing ⚠️
model/config.go 0.00% 5 Missing and 2 partials ⚠️
cmd/daemon/main.go 0.00% 5 Missing ⚠️
cmd/cli/main.go 0.00% 1 Missing ⚠️
Flag Coverage Δ
unittests 36.92% <0.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
cmd/cli/main.go 0.00% <0.00%> (ø)
cmd/daemon/main.go 0.00% <0.00%> (ø)
model/config.go 59.09% <0.00%> (-2.82%) ⬇️
daemon/socket.go 58.15% <0.00%> (-3.50%) ⬇️
daemon/client.go 57.14% <0.00%> (-9.53%) ⬇️
model/api_aicode_hooks.go 0.00% <0.00%> (ø)
commands/aicode_hooks.go 0.00% <0.00%> (ø)
daemon/aicode_hooks_processor.go 0.00% <0.00%> (ø)
commands/aicode_hooks_install.go 0.00% <0.00%> (ø)

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment thread commands/aicode_hooks.go
Comment on lines +196 to +203
go func() {
sendCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_, err := model.SendAICodeHooksData(sendCtx, req, endpoint)
if err != nil {
slog.Error("AICodeHooks: Direct HTTP send failed", slog.Any("err", err))
}
}()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Fire-and-forget goroutine in CLI process exits before HTTP request completes

In sendAICodeHooksDirect, the HTTP request is dispatched in a background goroutine (go func() at line 196). However, this function is called from the CLI command handler commandAICodeHooks which returns nil immediately after at commands/aicode_hooks.go:160. Once the command handler returns, app.Run() in cmd/cli/main.go:108 completes, CloseLogger() runs, and the process exits — killing the goroutine before the HTTP request can complete. This means the direct HTTP fallback path (used when the daemon socket is unavailable) will silently lose all event data. The fix is to make the HTTP call synchronous (remove the go func() wrapper), since the hook is already registered as async: true in Claude Code's settings, so Claude Code won't be blocked waiting for the shelltime process.

Suggested change
go func() {
sendCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_, err := model.SendAICodeHooksData(sendCtx, req, endpoint)
if err != nil {
slog.Error("AICodeHooks: Direct HTTP send failed", slog.Any("err", err))
}
}()
sendCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_, err := model.SendAICodeHooksData(sendCtx, req, endpoint)
if err != nil {
slog.Error("AICodeHooks: Direct HTTP send failed", slog.Any("err", err))
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new feature for tracking AI coding tool hook events, including a CLI command (aicode-hooks) to process incoming events from stdin, daemon-side processing for batching and sending these events to a backend, and install/uninstall subcommands to configure AI tools like Claude Code, Codex, and Cursor to trigger these hooks. Review comments highlight several areas for improvement: ensuring platform-independent path handling by using os.UserHomeDir() and filepath.Join instead of os.ExpandEnv("$HOME") or os.Getenv("HOME"), refactoring repetitive JSON parsing in commandAICodeHooks for better readability and maintainability (ideally by direct unmarshaling or using a library like mapstructure), simplifying conditional logic in uninstallGenericHooks using strings.HasPrefix, and improving efficiency in AICodeHooksProcessor by directly decoding socketMsg.Payload into AICodeHooksEventData using mapstructure instead of inefficient marshal/unmarshal cycles.

Comment thread commands/aicode_hooks.go
func commandAICodeHooks(c *cli.Context) error {
ctx, span := commandTracer.Start(c.Context, "aicode-hooks", trace.WithSpanKind(trace.SpanKindClient))
defer span.End()
SetupLogger(os.ExpandEnv("$HOME/" + model.COMMAND_BASE_STORAGE_FOLDER))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The use of os.ExpandEnv("$HOME/" + ...) is not platform-independent and violates the repository's general rules. On Windows, $HOME is not typically set. Please use os.UserHomeDir() to get the user's home directory and path/filepath.Join to construct the path.

For example:

homeDir, err := os.UserHomeDir()
if err != nil {
    slog.Error("Failed to determine home directory", "err", err)
    return err
}
SetupLogger(filepath.Join(homeDir, model.COMMAND_BASE_STORAGE_FOLDER))
References
  1. For platform-independent paths, use filepath.Join to combine segments and os.UserHomeDir() to get the home directory, rather than hardcoding path separators or environment variables like $HOME.

Comment thread commands/aicode_hooks.go
Comment on lines +79 to +139
// Parse common fields from raw payload
if v, ok := rawPayload["hook_event_name"].(string); ok {
eventData.HookEventName = v
}
if v, ok := rawPayload["session_id"].(string); ok {
eventData.SessionID = v
}
if v, ok := rawPayload["cwd"].(string); ok {
eventData.Cwd = v
}
if v, ok := rawPayload["permission_mode"].(string); ok {
eventData.PermissionMode = v
}
if v, ok := rawPayload["model"].(string); ok {
eventData.Model = v
}
if v, ok := rawPayload["tool_name"].(string); ok {
eventData.ToolName = v
}
if v, ok := rawPayload["tool_input"].(map[string]any); ok {
eventData.ToolInput = v
}
if v, ok := rawPayload["tool_response"].(map[string]any); ok {
eventData.ToolResponse = v
}
if v, ok := rawPayload["tool_use_id"].(string); ok {
eventData.ToolUseID = v
}
if v, ok := rawPayload["prompt"].(string); ok {
eventData.Prompt = v
}
if v, ok := rawPayload["error"].(string); ok {
eventData.Error = v
}
if v, ok := rawPayload["is_interrupt"].(bool); ok {
eventData.IsInterrupt = v
}
if v, ok := rawPayload["agent_id"].(string); ok {
eventData.AgentID = v
}
if v, ok := rawPayload["agent_type"].(string); ok {
eventData.AgentType = v
}
if v, ok := rawPayload["last_assistant_message"].(string); ok {
eventData.LastMessage = v
}
if v, ok := rawPayload["stop_hook_active"].(bool); ok {
eventData.StopHookActive = v
}
if v, ok := rawPayload["notification_type"].(string); ok {
eventData.NotificationType = v
}
if v, ok := rawPayload["notification_message"].(string); ok {
eventData.NotificationMessage = v
}
if v, ok := rawPayload["session_end_reason"].(string); ok {
eventData.SessionEndReason = v
}
if v, ok := rawPayload["transcript_path"].(string); ok {
eventData.TranscriptPath = v
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This large block of type assertions to parse fields from rawPayload is repetitive and can be hard to maintain. Consider refactoring this to improve readability and reduce boilerplate. One approach is to define helper functions for extracting typed values from the map. For example:

func getString(payload map[string]any, key string) string {
    if v, ok := payload[key].(string); ok {
        return v
    }
    return ""
}

// ... in commandAICodeHooks ...
eventData.HookEventName = getString(rawPayload, "hook_event_name")

An even better approach would be to unmarshal the JSON directly into the model.AICodeHooksEventData struct, which would eliminate this manual parsing entirely.


// installClaudeCodeHooks reads ~/.claude/settings.json, merges hooks, and writes back
func installClaudeCodeHooks() error {
settingsPath := filepath.Join(os.Getenv("HOME"), ".claude", "settings.json")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The use of os.Getenv("HOME") is not platform-independent and violates the repository's general rules. Please use os.UserHomeDir() to get the user's home directory. This issue is present in multiple functions in this file (e.g., lines 241, 297, 303, 309, 315).

A fix would look like this:

homeDir, err := os.UserHomeDir()
if err != nil {
    return fmt.Errorf("could not get user home directory: %w", err)
}
settingsPath := filepath.Join(homeDir, ".claude", "settings.json")
References
  1. For platform-independent paths, use filepath.Join to combine segments and os.UserHomeDir() to get the home directory, rather than hardcoding path separators or environment variables like $HOME.

Comment on lines +389 to +393
if cmd == "shelltime aicode-hooks" ||
cmd == "shelltime aicode-hooks --source=codex" ||
cmd == "shelltime aicode-hooks --source=cursor" {
continue // Skip shelltime hooks
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This condition can be simplified using strings.HasPrefix to make it more concise and robust against future additions of source-specific flags.

                                if strings.HasPrefix(cmd, "shelltime aicode-hooks") {
					continue // Skip shelltime hooks
				}

Comment on lines +121 to +131
payloadBytes, err := json.Marshal(socketMsg.Payload)
if err != nil {
slog.Error("AICodeHooks: Failed to marshal payload", slog.Any("err", err))
return
}

var eventData model.AICodeHooksEventData
if err := json.Unmarshal(payloadBytes, &eventData); err != nil {
slog.Error("AICodeHooks: Failed to parse event data", slog.Any("err", err))
return
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block of code marshals a map[string]interface{} to JSON and then immediately unmarshals it back to a struct. This is inefficient. A better approach is to use a library like github.com/mitchellh/mapstructure to directly decode the map into the struct. You'll need to add import "github.com/mitchellh/mapstructure" to your imports.

Suggested change
payloadBytes, err := json.Marshal(socketMsg.Payload)
if err != nil {
slog.Error("AICodeHooks: Failed to marshal payload", slog.Any("err", err))
return
}
var eventData model.AICodeHooksEventData
if err := json.Unmarshal(payloadBytes, &eventData); err != nil {
slog.Error("AICodeHooks: Failed to parse event data", slog.Any("err", err))
return
}
var eventData model.AICodeHooksEventData
if err := mapstructure.Decode(socketMsg.Payload, &eventData); err != nil {
slog.Error("AICodeHooks: Failed to parse event data", slog.Any("err", err))
return
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants