Recently, I was trying to solve the issue of Claude modifying files without user consent. I created a hook for this purpose.
Then I thought: "What happens if the last task given to a sub-agent is a modification?"
When I checked, it turned out that if the sub-agent's work is blocked by the hook during its operation, the sub-agent's work history is blocked and reported as failed.
It's a waste of tokens.
Fortunately, I found a branch that can distinguish sub-agents, and by setting up a branch in the hook to trigger an "ask" before modification, I was able to solve the problem.
I was worried that the "ask" would be ignored in yolo mode, so I tested it, but fortunately, setting the "ask" in the hook overrides it, confirming normal operation.
Since I was already looking into Claude's code, I decided to also look into the previously installed read_once hook. The read_once hook is structured so that if a file is read twice, a warning is sent to the LLM. However, I had doubts about whether it actually worked.
When I tested it, no warning was actually sent to the LLM.
Upon further investigation, I discovered that the field of the hook was incorrect. When I asked Claude to test it, he suddenly started digging into the Claude binary.
Somehow, I managed to solve the problem, and then a sudden thought occurred: "Could we also do reverse engineering with the Claude Code source?"
I told Claude the location of the source code and asked him to check for anything applicable to our hook. He discovered many undocumented hook options.
I've tested some of them, so feel free to take a look if you're interested.
🔌 Claude Code Hook Output Field Complete Summary
Reference for hook output fields, verified through source code (src/types/hooks.ts v2.1.88) + binary analysis (v2.1.123) + live experiments.
Field Role Comparison
Field | Location | Target | Action | Verification |
|---|
systemMessage
| Top Level | Human (TUI Display) | Displayed in UI only, not injected into AI context | ✅ |
hookSpecificOutput.additionalContext
| Inside hookSpecificOutput | AI Model | Injected into AI context with | ✅ |
hookSpecificOutput.permissionDecisionReason
| Inside hookSpecificOutput | Logs/UI | Not injected into AI context on allow, injected on deny | ✅ |
hookSpecificOutput.updatedInput
| Inside hookSpecificOutput | Tool Input | Replaces input value before tool execution | ✅ |
hookSpecificOutput.updatedMCPToolOutput
| Inside hookSpecificOutput | MCP Tool Output | Replaces MCP tool result value | Unverified |
Source Code Basis (src/types/hooks.ts)
// Top Level Field
systemMessage: z.string().describe('Warning message shown to the user').optional()
// Inside hookSpecificOutput — PreToolUse
z.object({
hookEventName: z.literal('PreToolUse'),
permissionDecision: permissionBehaviorSchema().optional(),
permissionDecisionReason: z.string().optional(),
updatedInput: z.record(z.string(), z.unknown()).optional(), // Tool input modification
additionalContext: z.string().optional(), // AI context injection
})
// Inside hookSpecificOutput — PostToolUse
z.object({
hookEventName: z.literal('PostToolUse'),
additionalContext: z.string().optional(),
updatedMCPToolOutput: z.unknown().optional(), // MCP output modification
})
Internal Processing Flow (src/utils/hooks.ts):
// systemMessage → hook_system_message (TUI display only)
if (result.systemMessage) {
yield { message: createAttachmentMessage({ type: 'hook_system_message', ... }) }
}
// additionalContext → hook_additional_context (AI context injection)
if (result.additionalContext) {
yield { additionalContexts: [result.additionalContext] }
}
// → createAttachmentMessage({ type: 'hook_additional_context', content: [...] })
Verified Output Format
PreToolUse warn mode (allow + AI context injection)
jq -cn --arg r "$REASON" \
'{"systemMessage":$r,"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","additionalContext":$r}}'
PreToolUse deny mode
{
"hookSpecificOutput": {
"permissionDecision": "deny",
"permissionDecisionReason": "Block reason"
}
}
PreToolUse updatedInput — Tool input modification
# Intercept and modify Bash commands
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": {
"command": "echo OVERRIDDEN" # Bash tool
# "file_path": "/new/path" # Read tool
}
}
}))
PostToolUse additionalContext
{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "Content to inject into AI context after tool execution"
}
}
Live Experiment Results (2026-05-01)
Experiment 1: additionalContext injection verification
additionalContext with instruction ACKNOWLEDGE: Write X on the first line of the response inserted → AI instruction execution confirmed.
Experiment 2: updatedInput — Bash command modification ✅
# Actual executed command
echo HOOK_TEST_ORIGINAL
# Output
HOOK_TEST_OVERRIDDEN ← hook replaced
Experiment 3: updatedInput — Read file path redirection ✅
Read("/tmp/REDIRECT_SOURCE.txt") request
→ hook replaces file_path with "/tmp/REDIRECT_TARGET.txt"
→ TARGET file content returned
Experiment 4: PostToolUse additionalContext ✅
After Bash tool execution, hook's additionalContext injected as into AI context confirmed.
additionalContext supported hook events
Events with additionalContext field in source code:
hookEventName | additionalContext |
|---|
PreToolUse | ✅ |
PostToolUse | ✅ |
PostToolUseFailure | ✅ |
UserPromptSubmit | ✅ |
SessionStart | ✅ |
Setup | ✅ |
SubagentStart | ✅ |
Notification | ✅ |
Other Notable Fields
|
Field
|
hook
|
Description
|
|
updatedInput
|
PreToolUse
|
Tool input manipulation (Bash command, Read file_path etc.)
|
|
updatedMCPToolOutput
|
PostToolUse
|
Replace MCP tool output value
|
|
initialUserMessage
|
SessionStart
|
Inject the first message at session start
|
|
watchPaths
|
SessionStart / CwdChanged
|
Register file watch paths
|
|
retry
|
PermissionDenied
|
Whether to retry after permission denial
|
|
updatedPermissions
|
PermissionRequest
|
Dynamically change permissions
|
read-once hook application details
~/.claude/read-once/hook.sh warn mode added additionalContext (2026-05-01):
# Before change (AI context not injected)
jq -cn --arg r "$REASON" \
'{"systemMessage":$r,"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
# After change (AI context injected)
jq -cn --arg r "$REASON" \
'{"systemMessage":$r,"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","additionalContext":$r}}'
diff path (warn mode) also modified + heredoc → jq replacement (special character safety).
🤖 Claude Code Sub-agent Hook Detection and Control Pattern
Pattern for detecting and controlling sub-agents in Claude Code when they attempt file modifications (Edit/Write/Bash writing) using the PreToolUse hook. Based on experimental observations and official documentation.
Key Findings
1. Session ID Sharing
Sub-agents share the same session_id with their parent. Since the UserPromptSubmit hook only executes when there is a user message, sub-agents do not create separate prompt files. Consequently, they read the parent's user message file as is.
2. Detecting Sub-Agents using the agent_id Field
The JSON received by the PreToolUse hook can be used to distinguish between a main agent and a sub-agent:
|
Field
|
Main Agent
|
Sub-agent
|
Logic Used
|
|
agent_id
|
None
|
Present (e.g., aaa6abfab...)
|
✅ Used
|
|
agent_type
|
None
|
Present (e.g., general-purpose)
|
❌ Not Used
|
Official documentation confirmation: "agent_id and agent_type are populated when the hook fires inside a subagent."
While agent_type was observed to be general-purpose in arbitrarily called sub-agents, its distinction from skill-based calls is unclear. Therefore, only the presence or absence of agent_id is used for detection, as it provides sufficient reliability.
3. Sub-Agents Do Not Automatically Inherit Parent Agent Permissions
Official documentation: "Subagents do not automatically inherit parent agent permissions."
When a skill is called, the user message includes the skill name and passes through the whitelist. However, sub-agents spawned autonomously by Claude based on the parent user message may accidentally pass through or be blocked.
Comparison of permissionDecision Actions
Value | Action | AI Context Injection |
|---|
| Just pass through | — |
| Completely block | Reason is passed to the sub-agent |
| User UI approval dialog | Not passed to AI |
| Defer for later (TypeScript only) | — |
Priority: > > >
Note
goes directly to the user UI and is not injected into the AI context. passes a reason to the sub-agent context, allowing the sub-agent to understand and report the reason.
Application Patterns
Code that detects sub-agents and branches to (shell command hook):
raw = sys.argv[1]
try:
d = json.loads(raw)
# Sub-agent detection → branch to ask
agent_id = d.get("agent_id", "")
if agent_id:
file_path = d.get("tool_input", {}).get("file_path", "?")
reason = f"Sub-agent({agent_id[:8]}) is requesting file modification: {file_path}"
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "ask",
"permissionDecisionReason": reason
}
}))
sys.exit(0)
# Remaining logic (for main agent)
...
Warning
may be automatically approved when is . Check the environment's permission_mode if actual user confirmation is required.
Important
Even in yolo mode, hook is not bypassed. Yolo mode automatically approves Claude Code's general permission prompts, but hooks intervene at a higher layer. The hook's always displays a user dialog regardless of whether yolo mode is active or not. → A reliable safety mechanism for controlling sub-agent modifications.
Hooks Applied to This Bolt
File | Target | Applied Content |
|---|
| Edit / Write | sub-agent → |
| Bash write command | sub-agent → |