Skip to content

fix(hooks): agent-tracker never ran — reads a non-existent env var - #130

Open
mt-alarcon wants to merge 1 commit into
evolution-foundation:mainfrom
mt-alarcon:fix/agent-tracker-hook-event
Open

fix(hooks): agent-tracker never ran — reads a non-existent env var#130
mt-alarcon wants to merge 1 commit into
evolution-foundation:mainfrom
mt-alarcon:fix/agent-tracker-hook-event

Conversation

@mt-alarcon

@mt-alarcon mt-alarcon commented Aug 16, 2026

Copy link
Copy Markdown

The agent-tracker.sh hook reads the event from $CLAUDE_HOOK_EVENT, a variable Claude Code does not export. With EVENT empty, no branch ever executes — so the hook has never written a single entry, while being registered in settings.json and exiting 0 every time.

Confirm in one command:

rg CLAUDE_HOOK_EVENT

It appears only in the hook that reads it, never in anything that sets it.

Proof by state: .claude/agent-status.json stays at its initialization value, {"active_agents":[],"last_updated":""}. An empty last_updated means even the Stop branch — which only writes a timestamp — never ran.

This is hard to notice because three conditions stack: the hook always exits 0 (correct, per the hook contract), every internal call ends in 2>/dev/null, and in our install nothing actually read agent-status.json, so the empty file never looked wrong.

Three defects, each silent on its own

  1. The event source does not exist. Fixed by reading argv[1] — the same pattern the plugin dispatcher already uses in settings.json — with a fallback to the hook_event_name field of the stdin payload.

  2. The payload parser could not match the real payload. It used grep -o anchored on "tool_name":" — no space after the colon — while the actual payload is pretty-printed ("tool_name": "Agent"). Replaced with a json parse. This one was hidden behind the first: fixing only the env var would have left the hook silent.

  3. $DESCRIPTION was interpolated into a python3 -c string, so a quote or apostrophe in the description broke the script — and 2>/dev/null swallowed it. Values now go through argv.

One behavior addition

Adds a PostToolUse branch. The hook previously only appended on start and cleared everything on Stop, so active_agents could never reflect what is running — only what had ever started. Since the stated purpose is to "show which agents are running in real-time", entries need to leave the list when an agent finishes.

Registering it requires one entry in settings.json:

{
  "matcher": "Agent",
  "hooks": [{ "type": "command", "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/agent-tracker.sh\" PostToolUse" }]
}

The fallback means existing registrations keep working unchanged — the event is then taken from the payload.

Verification

Tested against: the real pretty-printed payload; the payload-only fallback (no argv); a non-Agent tool (must not record); malformed input (still exits 0, per contract); and Stop.

Control: ran the previous version with the same real payload — it writes nothing, leaving last_updated empty. That is the current behavior on main.

Found while auditing multi-agent orchestration in a downstream install. The same defect is present in any EvoNexus install with this hook registered.

Summary by Sourcery

Fix the agent activity tracking hook so it correctly records agent lifecycle events and keeps the status file in sync with currently running agents.

New Features:

  • Track agent completion via a new PostToolUse event so active_agents reflects agents that are currently running, not just those that have started.

Bug Fixes:

  • Read the hook event from argv and JSON payload instead of a non-existent CLAUDE_HOOK_EVENT env var so the hook actually executes its branches.
  • Parse the hook payload as JSON instead of via brittle grep patterns so pretty-printed payloads are correctly handled.
  • Pass description and other dynamic values to the Python helper via argv to avoid breaking on quotes or special characters and silently failing.

Enhancements:

  • Make the agent tracker more robust to malformed input and missing status files while maintaining a zero-exit behavior per the hook contract.

`agent-tracker.sh` reads the hook event from `$CLAUDE_HOOK_EVENT`, which Claude
Code does not export. With EVENT empty no branch ever executes, so the hook has
never written a single entry — while being registered in settings.json and
exiting 0 on every invocation.

How to confirm in one command:

    rg CLAUDE_HOOK_EVENT

It only appears in the hook that reads it, never in anything that sets it.

Proof by state: `.claude/agent-status.json` stays at its initialization value,
`{"active_agents":[],"last_updated":""}`. An empty `last_updated` means even the
`Stop` branch (which only writes a timestamp) never ran.

Three defects, each silent on its own:

1. The event source does not exist. Fixed by reading argv[1] — the same pattern
   the plugin dispatcher already uses in settings.json — with a fallback to the
   `hook_event_name` field of the stdin payload.

2. The payload parser could not match the real payload. It used
   `grep -o '"tool_name":"[^"]*"'`, with no space after the colon, while the
   actual payload is pretty-printed (`"tool_name": "Agent"`). Replaced by a json
   parse. This defect was hidden behind the first one.

3. `$DESCRIPTION` was interpolated into `python3 -c '...'`, so a quote or
   apostrophe in the description broke the script — and `2>/dev/null` swallowed
   it. Values are now passed via argv.

Also adds a `PostToolUse` branch: the hook previously only appended on start and
cleared everything on `Stop`, so `active_agents` could never reflect what is
actually running — only what had ever started. Consumers reading "which agents
are running in real-time" got a list that only grew.

Verified with the real pretty-printed payload, with the payload-only fallback,
with a non-Agent tool, with malformed input (still exits 0, per the hook
contract), and against the previous version as a control — which writes nothing.
@sourcery-ai

sourcery-ai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Reviewer's Guide

Fixes the agent activity tracker hook so it correctly receives hook events, robustly parses the JSON payload, safely handles descriptions, and adds support for PostToolUse to keep the active_agents list in sync with currently running agents.

Sequence diagram for agent tracker hook event handling

sequenceDiagram
    actor ClaudeCode
    participant Hook as agent-tracker.sh
    participant Python as python3_block
    participant Status as agent-status.json

    ClaudeCode->>Hook: bash agent-tracker.sh PreToolUse
    ClaudeCode->>Hook: stdin JSON payload

    alt EVENT passed as argv[1]
        Hook->>Hook: EVENT=${1}
    else EVENT missing
        Hook->>Hook: EVENT from hook_event_name in payload
    end

    alt EVENT is PreToolUse or PostToolUse
        Hook->>Python: python3 -c (parse payload)
        Python->>Python: json.load
        Python->>Python: check tool_name == Agent
        alt EVENT is PreToolUse
            Python->>Python: append active_agents entry
        else EVENT is PostToolUse
            Python->>Python: remove matching active_agents entry
        end
        Python->>Status: json.dump
    else EVENT is Stop
        Hook->>Status: echo cleared active_agents
    end

    Hook->>ClaudeCode: exit 0
Loading

File-Level Changes

Change Details Files
Change event sourcing so the hook receives the event via argv with a JSON payload fallback instead of an undefined environment variable.
  • Remove reliance on CLAUDE_HOOK_EVENT and initialize EVENT from argv[1].
  • Read stdin into INPUT and, when EVENT is empty, derive the event name from the payload's hook_event_name field via a small Python JSON parser.
.claude/hooks/agent-tracker.sh
Replace brittle grep-based payload parsing with a Python JSON parser and extend it to support both PreToolUse and PostToolUse.
  • Parse the pretty-printed payload JSON in Python, exiting quietly on malformed input.
  • Filter non-Agent tools early by checking payload.tool_name.
  • Extract agent type and description from either top-level fields or tool_input and default agent type to general-purpose.
  • Share the same JSON parsing logic for both PreToolUse and PostToolUse events.
.claude/hooks/agent-tracker.sh
Make status file updates robust and reflect currently running agents instead of all historically started agents.
  • Load and update .claude/agent-status.json via Python, initializing a default structure on read failure.
  • On PreToolUse, append an entry with agent, description, and started_at, keeping only the last 20 entries.
  • On PostToolUse, remove the most recent matching agent/description entry so active_agents tracks what is actually running.
  • Always update last_updated to the current time and ensure the script exits 0 at the end.
  • Preserve Stop behavior to clear active_agents and set last_updated to now.
.claude/hooks/agent-tracker.sh

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • The PostToolUse removal logic relies on matching both agent and description, which may be brittle if descriptions are not stable or can collide; consider adding a stable identifier (e.g., an ID from the payload) to reliably pair start/stop events.
  • The script now unconditionally reads all of stdin into INPUT, even for events that do not require the payload (e.g., Stop); if hooks may receive large payloads or be chained, consider guarding the read so it only occurs for events that actually need it.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The PostToolUse removal logic relies on matching both `agent` and `description`, which may be brittle if descriptions are not stable or can collide; consider adding a stable identifier (e.g., an ID from the payload) to reliably pair start/stop events.
- The script now unconditionally reads all of stdin into `INPUT`, even for events that do not require the payload (e.g., `Stop`); if hooks may receive large payloads or be chained, consider guarding the read so it only occurs for events that actually need it.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

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