[SECURITY] PreToolUse hooks not enforced on subagent tool calls, allowing security bypass
Summary
PreToolUse hooks configured in ~/.claude/settings.json are not enforced when subagents spawned via the Task tool make their own tool calls. This creates a security vulnerability where restrictions that apply to the main agent can be completely bypassed by spawning a subagent.
Steps to Reproduce
- Configure a PreToolUse hook to restrict file writes outside the project directory:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit|Bash",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/restrict-operations.py",
"timeout": 5
}
]
}
]
}
}
- Hook script blocks writes outside project directory (exits with code 1 for unauthorized paths)
- Try to write a file outside project with main agent:
- ✅ Correctly blocked by PreToolUse hook
- Spawn a subagent with the same request:
Task tool with python-coder subagent
Prompt: "Create a file at /Users/username/test.py"
- ❌ Bypasses the hook - file is created successfully
Expected Behavior
PreToolUse hooks should apply recursively to all agents in the execution tree:
- Main agent's tool calls → hook enforced
- Subagent's tool calls → hook enforced
- Sub-subagent's tool calls → hook enforced
Actual Behavior
PreToolUse hooks only apply to the agent where settings.json is loaded:
- Main agent's tool calls → hook enforced ✅
- Subagent's tool calls → hook NOT enforced ❌
Security Impact
This allows complete bypass of security restrictions:
- File access controls - Can write/read files anywhere on the system
- Command restrictions - Can execute unrestricted bash commands
- Self-modification - Can modify or delete the hooks themselves
- Privilege escalation - Any security boundary can be bypassed
Attempted Workarounds
1. SubagentStart Hook
Added SubagentStart hook to intercept subagent spawning and analyze prompts for suspicious patterns. Limitation: Can only do heuristic analysis of the prompt text, cannot actually enforce tool-level restrictions within the subagent.
2. Intercept Task Tool
Added Task tool to PreToolUse matcher to block suspicious subagent prompts. Limitation: Prompt analysis is unreliable - many legitimate prompts contain file paths, and adversarial prompts can obfuscate intent.
Proposed Solutions
- Global Hook Enforcement (Preferred)
- Hooks configured in
~/.claude/settings.jsonshould apply system-wide to all agents - Each subagent inherits parent's hook configuration
- Hooks execute in subagent's context when subagent makes tool calls
- Explicit Hook Inheritance Flag
- Add
inherit: trueoption to hook configuration - Allows opt-in recursive enforcement for security-critical hooks
- Subagent Security Context
- Add security context parameter to Task tool
- Explicitly declare which hooks must be enforced on subagent
Environment
- Platform: macOS (Darwin 25.2.0)
- Claude Code Version: Latest (using claude-sonnet-4-5-20250929)
- Hook Types Affected: PreToolUse
- Tools Affected: Write, Edit, Bash, NotebookEdit (any tool called by subagents)
Related Issues
- #20221 - SubagentStop hooks don't prevent termination
- #16424 - Expose Agent Context in Hook Event Payloads
- #16126 - Add agent identity to PreToolUse hook data
- #18653 - Tool result transform hook for content sanitization
Example Use Cases Requiring This Fix
- Corporate environments - Enforce write restrictions to prevent data exfiltration
- Sandboxed execution - Prevent agents from escaping sandbox via subagents
- Code review automation - Ensure read-only analysis can't be bypassed
- Credential protection - Block access to sensitive files/directories
- Audit logging - Ensure all tool calls are logged, not just main agent
Additional Context
This is a fundamental security architecture issue. While hooks provide excellent visibility and control for the main agent, the lack of recursive enforcement makes them insufficient for security-critical use cases. Any security boundary enforced via hooks can be trivially bypassed by using the Task tool.
16 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
This addresses a gap in the current hook architecture's security model. PreToolUse can block tool execution, but as you've demonstrated, this protection doesn't extend to subagents - any security boundary can be bypassed by spawning a Task.
Related: #18653 identifies another architectural gap where tool results cannot be sanitized before context ingestion. PreToolUse can block, PostToolUse can observe, but neither can transform content before it enters Claude's context window, leaving no mitigation layer against prompt injection via external content.
Together these suggest the hook system needs architectural attention for security use cases, not just point fixes.
Confirmed: Reproduced with E2E Test
I've empirically confirmed this issue while testing a PreToolUse-based safety plugin.
Test Results
Setup: Plugin-level PreToolUse hook with empty matcher (applies to all tools), deny rule for
curlTest: Spawned 2 subagents in parallel via Task tool - one to run
find, one to runcurlFindings:
curlcommandConclusion: Plugin-level PreToolUse hooks do NOT fire for subagent tool calls.
Supporting Solution 1: Global Hook Enforcement
I strongly support the proposed "Global Hook Enforcement" solution - hooks configured in settings should apply recursively to all agents in the execution tree.
This is critical for security use cases where restrictions must be enforceable, not bypassable.
Resources
Has there been any prioritization of this? This is a non-starter for subagent usage for us, as all guardrails are ignored.
This is a fundamental architectural issue — hooks run at the process level, but subagents spawn new processes that don't inherit hook configuration.
An alternative approach is scanning at the API/middleware layer rather than relying on hooks. Sentinel AI takes this approach — it can run as:
``
json
``{
"mcpServers": {
"safe-server": {
"command": "sentinel",
"args": ["mcp-proxy", "--", "your-mcp-server"]
}
}
}
Since these approaches operate at the transport layer, they're immune to the subagent bypass — the scanning happens on every tool call, regardless of which agent initiated it.
The hook system is still valuable for quick policy enforcement, but for security-critical scanning, an external safety layer that can't be bypassed by spawning subprocesses is more robust.
This is a real gap. We work around it by enforcing at the shell execution boundary rather than the tool call boundary.
Our hook (https://github.com/tech-and-ai/claude-rule-enforcer) intercepts commands at the Bash tool level. Since every subagent ultimately executes through the same shell, enforcement applies regardless of which agent tier spawned the command.
Two layers: L1 regex for instant blocks (<10ms), L2 LLM review for context-dependent decisions. The architecture means even if PreToolUse doesn't fire for subagents, the gate still catches destructive commands.
Yikes, my hooks aren't protecting me. This is a serious issue.
Please try the CRE https://github.com/tech-and-ai/claude-rule-enforcer
I built something for exactly this. CRE (Claude Rule Enforcer) is a two-layer enforcement system that sits between the agent and the OS via pre-tool hooks.
L1: regex gate, blocks dangerous patterns instantly (<10ms). fork bombs, disk writes, force push, production SSH.
L2: LLM reads the conversation and checks "did the user actually ask for this?" if no user approval exists, it blocks. no retry bypass. the user has to explicitly say "yes do it" in chat.
It also self-protects. the agent cannot turn CRE off even if instructed to. hooks auto-restore if tampered with.
It works with OpenClaw's new tool:pre hooks, plus Claude Code, Cursor, Windsurf, Copilot, Codex, Amp. ships as an MCP server too for the intelligence layer.
open source (BSL 1.1): https://github.com/tech-and-ai/claude-rule-enforcer
site: https://ai-cre.uk
I've also reproduced this issue.
In addition, the PreToolUse and PostToolUse hook payloads don't include the calling agent_id. I'd like to have conditional logic for how the PreToolUse and PostToolUse hooks perform depending on which subagent called the tool. To accomplish this, I'm currently tracking the agent_id returned in the SubagentStart event payload and mapping it to my specified subagents in .claude/agents. Then, when a PreToolUse or PostTooleUse event arrives, I would check the agent_id in the payload, compare it to which of my agents are mapped to that id, and then run the conditional logic in my hook script.
Perhaps including agent_id in the payload of the PreToolUse and PostToolUse events should be a separate feature request... But then again, there are over 5000 of them right now, so... I'm slipping it in here! 😇
Claude wrote a bug report: worktree-permission-bypass-bug-report.md
I've reproduced the issue in a worktree using the new
isolation: worktreemode.Hook-level workaround: enforce security at user level + defense-in-depth
The root cause is that project-level
.claude/settings.jsonhooks only bind to the agent that loads them. User-level hooks (~/.claude/settings.json) inherit to subagents because they're loaded at the Claude Code process level.Move your security hooks from project settings to
~/.claude/settings.json:User-level hooks fire for both the main agent and subagents.
Even with user-level hooks, a subagent can still be given overly broad permissions. This hook limits what files subagents can write:
Then set scope per project:
echo "src/" > .claude/agent-scope.txtThis is a workaround. The proper fix needs Claude Code to propagate hook bindings to all agents in the execution tree. But user-level hooks + scope files cover the most common bypass vectors.
Signet is now available on the official Claude Code plugin marketplace:
/plugin install signet@claude-plugins-official
Every tool call gets Ed25519-signed and appended to a hash-chained audit log at ~/.signet/audit/. This covers all agents in a session, including subagents that might bypass PreToolUse hooks.
Query the trail:
signet audit --since 1h
signet audit --verify # verify hash chain integrity
Not a replacement for fixing hook inheritance — but a useful defense-in-depth layer while that gets resolved.
https://github.com/Prismer-AI/signet
We tested both v2.1.22 (the release from the day this was filed) and current main: in both, a settings.json PreToolUse hook matching Write that exits code 2 fires and blocks when a subagent spawned via Task calls Write, identically to the main agent.
A note on the project-vs-user-level workaround mentioned upthread: both .claude/settings.json and ~/.claude/settings.json merge into the same startup snapshot, so there's no difference between them for subagent inheritance.
Related improvement since this was filed:
• v2.1.69+: hook payloads now include
agent_id(set for subagent calls, absent for main-agent calls) andagent_type, so your hook script can branch on which agent made the call. Addresses @mathnathan's ask and #16126.Thank you @dicksontsai !
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.