[FEATURE] Expose Agent Context in Hook Event Payloads for Multi-Agent Observability
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 PreToolUse↔PostToolUse 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
- Minimal Surface Area: Add only the essential fields needed for agent identification
- Consistency: Use the same field names across all affected events
- Backward Compatibility: New fields are additive; existing integrations continue to work
- Leverage Existing Data: Fields should mirror what's already available in
SubagentStart/SubagentStopinternal 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:
- 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.
- Unnecessary Complexity: For observability and routing purposes, knowing the
agent_idandagent_typeis sufficient. Parent relationships can be inferred from session scope if needed. - 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.
20 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
This proposal addresses gaps in existing issues with a minimal, backward-compatible solution.
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.jsonat 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:This is exactly what this issue requests: exposing
agent_id,agent_type, andagent_transcript_pathin 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.
+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:Binary analysis confirms:
agent_idandagent_typealready 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.yes that would be reallyt nice
We ran into this exact problem — our
PreToolUsehooks 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(thetoolu_*ID of the current tool call) andtranscript_path(the main session transcript). Main agent tool calls appear in the main transcript as assistant messages withtool_usecontent 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 thetool_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
grepmay not find thetool_use_idyet. 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).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_idin the hook payload to replace all of this.See also #14859
🤖 Generated with Claude Code
+1
+1 this is something that has been really frustrating to deal with in our hooks. Thanks for the detailed investigation.
+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.jsonandtype:summarytranscript 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. Exposingagent_idin PostToolUse would solve both attribution and naming.+1 - The 3-field approach is the right minimal surface. Worth noting: the Agent SDK already passes
tool_use_idto SubagentStop callbacks and referencesparent_tool_use_idin the troubleshooting section, so the runtime already tracks these associations internally.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:
.py,.js,.ts, etc.)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 |
|---|---|
|
CLAUDECODEenv 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/SubagentStophooks + 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:
Compared to JSON stdin parsing (Option B in #16424), the env var:
[[ "$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.
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", thetool_parametersfield 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_promptevents), skills work (viatool_parameters.skill_nameon 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 justagent_typealone 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
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.
+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 --grepforensics. 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, andagent_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.
@AlexandrePh
This proposed only agent_id, agent_type and agent_transcript_path.
not agent_id, parent_agent_id, and agent_slug.
Please fix it. its is needed for observablitity
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.
Looks like they added
agent_idto 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
2.1.69
Added agent_id (for subagents) and agent_type (for subagents and --agent) to hook events
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.