[FEATURE] Expose Agent Context in Hook Event Payloads for Multi-Agent Observability

Resolved 💬 20 comments Opened Jan 6, 2026 by odoldotol Closed Mar 5, 2026

Preflight Checklist

  • [x] I have searched existing requests and found related issues (#7881, #14859)
  • [x] This is a single, focused feature request with a concrete implementation proposal
  • [x] This proposal addresses gaps in existing issues with a minimal, backward-compatible solution

Problem Statement

The Core Issue

When hook scripts execute for events like PreToolUse, PostToolUse, Notification, SubagentStop, or PreCompact, there is no way to determine which agent (main or subagent) triggered the event. All events share the same session_id regardless of their origin, making it impossible to build reliable observability, debugging, or orchestration tooling for multi-agent workflows.

Why Existing Workarounds Fail

| Approach | Why It Fails |
|----------|--------------|
| Process/Environment differentiation | Main agent and subagents run asynchronously in the same process. No PID, PPID, or environment variable distinguishes them. |
| Parsing transcript_path JSONL | Unreliable: (1) requires complex async file parsing, (2) transcript structure is undocumented and subject to change, (3) race conditions with concurrent subagents, (4) significant performance overhead. |
| Session-to-agent mapping | Breaks immediately with parallel subagents—mapping gets overwritten and produces incorrect attribution (detailed in #7881). |
| Using tool_use_id correlation | Only works for PreToolUsePostToolUse pairs; doesn't help identify the originating agent for other events. |

Affected Events

The following hook events can be triggered by subagents but provide no agent identification:

| Event | Can Fire from Subagent | Agent Info Available |
|-------|------------------------|---------------------|
| PreToolUse | ✅ Yes | ❌ No |
| PostToolUse | ✅ Yes | ❌ No |
| PostToolUseFailure | ✅ Yes | ❌ No |
| Notification | ✅ Yes | ❌ No |
| SubagentStart | N/A (announces subagent) | ⚠️ Partial* |
| SubagentStop | N/A (announces subagent) | ⚠️ Partial* |
| PreCompact | ✅ Yes | ❌ No |

*SubagentStart and SubagentStop events exist but currently lack consistent agent identification fields in their payloads.

Proposed Solution

Design Principles

  1. Minimal Surface Area: Add only the essential fields needed for agent identification
  2. Consistency: Use the same field names across all affected events
  3. Backward Compatibility: New fields are additive; existing integrations continue to work
  4. Leverage Existing Data: Fields should mirror what's already available in SubagentStart/SubagentStop internal handling

Specification

Add the following three fields to the stdin JSON payload only for events triggered by subagents:

interface SubagentContext {
  /** Unique identifier for this subagent instance (e.g., "a1b2c3d4") */
  agent_id: string;
  
  /** 
   * Subagent type identifier from agent definition
   * (e.g., "code-reviewer", "frontend-developer", "ui-designer")
   */
  agent_type: string;
  
  /** 
   * Path to this subagent's transcript file
   * (e.g., "/home/user/.claude/projects/.../agent-{agent_id}.jsonl")
   */
  agent_transcript_path: string;
}

For main agent events: These fields are omitted (not present in the payload). Only subagents require explicit identification—the main agent is implicitly identified by the absence of these fields.

Example Payloads

Main Agent Event (Unchanged)

For main agent events, no new fields are added. The absence of agent fields implicitly indicates the main agent:

{
  "session_id": "abc-123",
  "hook_event_name": "PostToolUse",
  "tool_name": "Write",
  "tool_input": { "file_path": "/app/component.tsx", "content": "..." },
  "tool_response": { "success": true },
  "transcript_path": "/home/user/.claude/projects/.../abc-123.jsonl"
}
Subagent Event (New Fields Added)

For subagent events, the three identification fields are included:

{
  "session_id": "abc-123",
  "hook_event_name": "PostToolUse",
  "tool_name": "Write",
  "tool_input": { "file_path": "/app/styles.css", "content": "..." },
  "tool_response": { "success": true },
  "transcript_path": "/home/user/.claude/projects/.../abc-123.jsonl",
  "agent_id": "f7e8d9c0",
  "agent_type": "ui-designer",
  "agent_transcript_path": "/home/user/.claude/projects/.../agent-f7e8d9c0.jsonl"
}
Detection Logic in Hook Scripts
# Simple check: presence of agent_id indicates subagent
is_subagent = "agent_id" in event

This design avoids redundant fields for main agent events—only subagents require explicit identification.

Why NOT Include Parent References

Some related proposals suggest adding parent_agent_id or hierarchical fields. I recommend against this for the initial implementation:

  1. Flat Architecture: Claude Code's subagents operate in parallel within the same process, not in a true parent-child hierarchy. All subagents share the same session.
  2. Unnecessary Complexity: For observability and routing purposes, knowing the agent_id and agent_type is sufficient. Parent relationships can be inferred from session scope if needed.
  3. Scope Creep Risk: Hierarchical fields imply nested subagents, which isn't the current execution model and could create confusion.

The session_id already serves as the "parent" concept—all agents within a session share it.

Priority

High — This limitation blocks fundamental observability requirements for multi-agent workflows, which are increasingly common as users adopt custom subagents for specialized tasks.

Feature Category

Other

Use Cases Enabled

1. Agent-Specific Logging & Metrics

#!/usr/bin/env python3
import json, sys

event = json.load(sys.stdin)
tool_name = event.get("tool_name", "unknown")

# Determine agent type
if "agent_id" in event:
    agent_type = event["agent_type"]
else:
    agent_type = "main"

# Route metrics to agent-specific dashboards
send_metric(f"agent.{agent_type}.tool.{tool_name}.count", 1)
send_metric(f"agent.{agent_type}.tool.{tool_name}.latency", event.get("duration_ms", 0))

2. Agent-Specific Tool Policies

#!/usr/bin/env python3
import json, sys

event = json.load(sys.stdin)

# Only allow Write tool for specific agents
if event.get("tool_name") == "Write":
    # Main agent is always allowed
    if "agent_id" in event:
        allowed_subagents = ["frontend-developer", "backend-developer"]
        if event["agent_type"] not in allowed_subagents:
            print(f"Subagent '{event['agent_type']}' is not authorized to write files", file=sys.stderr)
            sys.exit(2)  # Block the tool call

3. Real-Time Multi-Agent Dashboards

#!/usr/bin/env python3
import json, sys
from datetime import datetime

event = json.load(sys.stdin)
is_subagent = "agent_id" in event

# Emit structured event for real-time visualization
dashboard_event = {
    "timestamp": datetime.now().isoformat(),
    "session_id": event["session_id"],
    "agent_id": event.get("agent_id"),  # None for main agent
    "agent_type": event["agent_type"] if is_subagent else None,
    "event_type": event["hook_event_name"],
    "tool": event.get("tool_name"),
    "is_subagent": is_subagent,
}
websocket_broadcast(dashboard_event)

4. Differential Compaction Handling

#!/usr/bin/env python3
import json, sys

event = json.load(sys.stdin)

if event["hook_event_name"] == "PreCompact":
    # Check if this is a subagent compaction
    if "agent_id" in event:
        # Backup subagent transcript separately
        backup_transcript(
            event["agent_transcript_path"],
            f"subagent-{event['agent_type']}-backup.jsonl"
        )
    else:
        # Main agent compaction - use standard backup
        backup_transcript(event["transcript_path"], "main-backup.jsonl")

Relationship to Existing Issues

| Issue | Focus | How This Proposal Differs |
|-------|-------|---------------------------|
| #7881 | SubagentStop identification | This proposal provides a unified solution across all hook events, not just SubagentStop |
| #14859 | Agent hierarchy + new hooks | This proposal is minimal and focused—no new hook types, no hierarchical fields, just essential identification |

This proposal can be seen as a foundational step that enables the broader observability features discussed in those issues without requiring architectural changes.

Implementation Considerations

Minimal Code Changes Expected

The agent_id, agent_type, and transcript path are already tracked internally for SubagentStart/SubagentStop processing. This proposal simply requests exposing these existing values in other hook event payloads.

Backward Compatibility

  • New fields are additive only
  • Existing hook scripts that don't use these fields continue to work unchanged
  • No changes to exit code semantics or JSON output schemas

Documentation Updates Needed

  • Update Hooks Reference with new fields in the "Hook Input" section
  • Add examples showing agent-aware hook implementations

Summary

| What | Details |
|------|---------|
| Request | Add agent_id, agent_type, and agent_transcript_path to hook event payloads for subagent-triggered events only |
| Scope | PreToolUse, PostToolUse, PostToolUseFailure, Notification, SubagentStop, PreCompact |
| Main Agent Events | Unchanged (no new fields) — absence of fields indicates main agent |
| Benefit | Enables reliable agent identification for observability, policy enforcement, and debugging |
| Complexity | Low — leverages existing internal agent tracking |
| Breaking Changes | None — purely additive |

---

Thank you for considering this proposal. I'm happy to provide additional technical details, test scenarios, or implementation suggestions if helpful.

View original on GitHub ↗

20 Comments

github-actions[bot] · 6 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/16126
  2. https://github.com/anthropics/claude-code/issues/7881
  3. https://github.com/anthropics/claude-code/issues/6885

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

odoldotol · 6 months ago

This proposal addresses gaps in existing issues with a minimal, backward-compatible solution.

odoldotol · 5 months ago

This issue is still needed for global hook agent identification

After reviewing recent changelog updates (particularly v2.1.0), I wanted to provide some context on the current state.

What v2.1.0 improved

v2.1.0 introduced hooks support in agent frontmatter, allowing hooks to be scoped to specific agents. This is a useful addition—you can now attach monitoring or policy hooks directly to individual agent definitions as a workaround for some scenarios.

Why this issue is still necessary

However, global hooks (defined in .claude/settings.json at project or user level) are still triggered by both the main agent and all subagents. When a global hook fires, there is still no way to determine:

  • Whether it was triggered by the main agent or a subagent
  • If it was a subagent, which specific subagent triggered it

This is exactly what this issue requests: exposing agent_id, agent_type, and agent_transcript_path in the hook event payload.

Summary

Agent-scoped hooks (v2.1.0) allow you to attach hooks to specific agents, but they don't solve the problem of identifying the source agent when a global hook is triggered. For centralized observability, logging, or policy enforcement across all agents, this feature request remains necessary.

kyzzen · 5 months ago

+1 for this proposal.

My use case: Building a context-tracking hook system that logs file operations per-agent. Currently impossible to attribute operations correctly when 3+ parallel agents run.

Evidence from testing: Spawned 3 parallel Explore agents - all file ops got attributed to a single agent because get_current_agent_id() returns stack top:

{"type": "glob", "agent_id": "a50287d"}  // All three agents' ops
{"type": "read", "agent_id": "a50287d"}  // get same agent_id
{"type": "glob", "agent_id": "a50287d"}  // from stack top

Binary analysis confirms: agent_id and agent_type already exist in SubagentStart/Stop payloads - just needs threading through to tool use hooks.

The minimal 3-field approach (agent_id, agent_type, agent_transcript_path) is exactly right. Hierarchical fields can come later if needed.

Butanium · 5 months ago

yes that would be reallyt nice

Butanium · 5 months ago

We ran into this exact problem — our PreToolUse hooks enforce different behavior for the main agent (force background bash, block large TaskOutput) that breaks subagents. Here's the workaround we're using in the meantime.

Detection: is this tool call from the main agent or a subagent?

The hook input includes tool_use_id (the toolu_* ID of the current tool call) and transcript_path (the main session transcript). Main agent tool calls appear in the main transcript as assistant messages with tool_use content blocks. Subagent tool calls only appear in their own JSONL transcripts under <session-dir>/subagents/.

So: grep -q <tool_use_id> <transcript_path> — if found, it's the main agent. If not, it's a subagent.

Follow-up: which subagent?

The subagent transcripts live at <transcript_path minus .jsonl>/subagents/agent-*.jsonl. Grep each for the tool_use_id — the one that matches is the calling subagent, and the agent ID is in the filename.

Handling transcript flush race:

The hook fires while the assistant message is being written to the transcript, so grep may not find the tool_use_id yet. We use exponential backoff (50ms → 4s): check main transcript, check subagent transcripts, sleep, retry. If neither matches after timeout, sys.exit(1) (fail open — tool proceeds, error logged in verbose mode only).

def is_main_agent_call(tool_use_id, transcript_path):
    wait = 0.05
    while wait <= 4.0:
        if grep_for(tool_use_id, transcript_path):
            return True
        for sa in glob(f"{transcript_path.replace('.jsonl', '')}/subagents/agent-*.jsonl"):
            if grep_for(tool_use_id, sa):
                return False
        time.sleep(wait)
        wait *= 2
    sys.exit(1)  # fail open

This works but is fragile — transcript structure is undocumented, the backoff adds latency to every guarded hook call, and concurrent subagents could hit race conditions. Would love to see agent_id in the hook payload to replace all of this.

See also #14859

🤖 Generated with Claude Code

ias-z · 5 months ago

+1

nicsuzor · 5 months ago

+1 this is something that has been really frustrating to deal with in our hooks. Thanks for the detailed investigation.

WarrenZhu050413 · 4 months ago

+1 — Built a full observability pipeline (PostToolUse → JSONL logger + SubagentStart/SubagentStop → marker files) and hit this exact wall.

Workaround we use: SubagentStart writes per-agent markers to /tmp/claude_subagent_{session_id}_{agent_id}, PostToolUse globs them to list active agents. With parallel subagents, we can only log all active agents — no way to attribute a specific tool call to a specific subagent.

Additional note: sessions-index.json and type:summary transcript entries (used for session auto-naming / /rename) appear to have stopped being written as of ~v2.1.40+. This breaks any hook-based attempt to correlate session names with agent activity. Exposing agent_id in PostToolUse would solve both attribution and naming.

mr-lee · 4 months ago

+1 - The 3-field approach is the right minimal surface. Worth noting: the Agent SDK already passes tool_use_id to SubagentStop callbacks and references parent_tool_use_id in the troubleshooting section, so the runtime already tracks these associations internally.

arwoxb24 · 4 months ago

Additional Use Case: Policy Enforcement via Environment Variable

This comment adds a complementary perspective to the excellent observability-focused proposal above — specifically the security/policy enforcement use case and an alternative implementation path.

Our Use Case: Orchestrator/Executor Pattern Enforcement

In delegation workflows, the main agent acts as an orchestrator and subagents execute implementation tasks. The goal is to enforce this separation via hooks:

  • Block the main agent from directly writing code files (Write/Edit tools on .py, .js, .ts, etc.)
  • Allow subagents to write those same files when delegated by the orchestrator

This is a compliance/guardrail pattern: the orchestrator should plan and delegate, not implement directly.

Why Current Workarounds Fail

We tested every available identifier:

| Identifier | Result |
|---|---|
| CLAUDECODE env var | Same in both |
| CLAUDE_CODE_ENTRYPOINT | Same in both |
| CLAUDE_CONFIG_DIR | Same in both |
| CLAUDE_SESSION_ID | Same in both |
| $$ (PID) | Same process — identical |
| $PPID | Same parent — identical |
| SubagentStart/SubagentStop hooks + state file | Race conditions with parallel subagents |

Result: impossible to distinguish orchestrator from executor in hook bash scripts.

Alternative Implementation: Environment Variable

In addition to the JSON stdin approach proposed above, an environment variable would be simpler for bash hooks:

# Option A: env var set before hook execution
CLAUDE_AGENT_TYPE=main   # or "subagent"

# Hook script can then do:
if [[ "$CLAUDE_AGENT_TYPE" == "main" ]]; then
  if [[ "$TOOL_NAME" == "Write" ]]; then
    echo "Orchestrator cannot write code directly — delegate to subagent" >&2
    exit 2
  fi
fi

Compared to JSON stdin parsing (Option B in #16424), the env var:

  • Requires no JSON parsing in bash scripts
  • Is accessible to any subprocess spawned by the hook
  • Can be checked with a simple [[ "$VAR" == "value" ]]

Both approaches (env var + JSON field) are complementary and could be shipped together.

Impact

| Without agent identification | With agent identification |
|---|---|
| Block ALL Write calls (breaks subagents) | Block only main-agent Write calls |
| Warn ALL (no enforcement possible) | Enforce orchestrator-executor separation |
| No delegation compliance hooks | Full hook-based delegation compliance |

This pattern benefits anyone building hook-based compliance, security, or delegation enforcement systems on top of Claude Code.

---

Strong +1 to this feature request. The env var addition would make the bash-hook use case particularly ergonomic.

arty-prague · 4 months ago

We're hitting this exact limitation building an OTLP-based observability pipeline to track Claude Code usage across our team.
Out of three metric categories we need — sessions, skills, agents — agent tracking is completely blocked.
When tool_name ="Task", the tool_parameters field is empty. We can see "some Task executed" but have no way to determine which subagent type was invoked (Explore, Plan, custom agents), which model it used, or attribute cost.

Sessions work (via user_prompt events), skills work (via tool_parameters.skill_name on Skill tool calls), but agent usage is a blind spot.

The three fields proposed here — agent_id, agent_type, agent_transcript_path — would unblock this entirely. Even just agent_type alone in hook payloads and OTLP events would be enough for the most critical use cases: usage dashboards, cost attribution per agent type, and anomaly alerting, usage of skills in agents.

Strong +1 to this feature request

amaksoft · 4 months ago

Very strong + 1 to this feature. Agent formatter doesn't really help to identify individual agents.
We struggle a lot trying to deduplicate per-agent work in our plugins.

If parallel agents use case is growing, being able to identify them is a must.

AlexandrePh · 4 months ago

+1 on this. Running multi-agent workflows (tribe-lead coordinating squad-workers in worktrees) and hitting this exact blind spot.

Specific pain point: Sub-agent git commits are unverifiable after the fact. A sub-agent claims it committed, but since its internal tool calls (Bash, Edit, Write) don't appear in OTEL, we can't confirm what actually happened without manual git log --grep forensics. The Task tool only emits the spawn and return spans — everything in between is a black box.

What would unblock us: Agent context in hook payloads (as proposed here) would work great, provided we can emit those hook events as OTEL spans to a collector. Our pipeline is OTEL Collector → ClickHouse, and we need sub-agent tool calls to show up as nested child spans under the parent Task span so we can query them in ClickHouse alongside everything else.

Concretely, if hook events included agent_id, parent_agent_id, and agent_slug (as proposed in #14859), and we could forward those as span attributes to the OTEL collector, that would fully close the observability gap for multi-agent coordination workflows.

The workaround today (PreToolUse hook writing to a flat audit log) works but loses the parent-child span relationship that makes ClickHouse queries useful.

odoldotol · 4 months ago

@AlexandrePh
This proposed only agent_id, agent_type and agent_transcript_path.
not agent_id, parent_agent_id, and agent_slug.

B1tMaster · 4 months ago

Please fix it. its is needed for observablitity

ThinkOffApp · 4 months ago

Would love to see this land. We run parallel agents across different models and the attribution problem is very real.

Right now we work around it with a receipt log (each agent action gets logged with the agent identity), but having agent_id and agent_type in hook payloads natively would simplify things a lot. parent_agent_id would be great too for tracking delegation chains.

For anyone dealing with this today, we built a receipt and audit system into IDE Agent Kit that tracks which agent did what. Not as clean as native hook support, but it works for production multi-agent setups.

Nxt3 · 4 months ago

Looks like they added agent_id to the hook event in 2.1.69.

Docs specifically mention the use cases described in this ticket.

https://code.claude.com/docs/en/hooks#common-input-fields

odoldotol · 4 months ago

2.1.69
Added agent_id (for subagents) and agent_type (for subagents and --agent) to hook events

github-actions[bot] · 4 months ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.