permissions.deny + PreToolUse hook did not block a Task-tool subagent from reading a denied file (blocked correctly on retest)
Description
A permissions.deny rule combined with a matching PreToolUse hook (on Read|Bash|Grep|Glob) that reliably blocks the main agent from reading a sensitive file pattern did not reliably block a Task/Agent-tool-spawned subagent from reading that same file — at least once in a session, the subagent returned real contents from a path that should have been denied. A follow-up test with a fresh subagent, same settings, same file, was correctly blocked.
Setup
~/.claude/settings.json (user-level), unchanged before/during/after the session:
{
"permissions": {
"deny": [
"Read(**/*.xcconfig)",
"Read(**/APIConstants.swift)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Bash|Grep|Glob",
"hooks": [
{
"type": "command",
"command": "input=$(cat); path=$(echo \"$input\" | jq -r '(.tool_input.file_path // .tool_input.command // .tool_input.pattern // \"\")'); echo \"$path\" | grep -qiE \"xcconfig|APIConstants\\.swift\" && echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Access to this file is blocked\"}}' || true"
}
]
}
]
}
}
No project-level settings existed to override this.
Steps observed (not yet minimized to a deterministic repro)
- In a project containing a file matching the deny pattern (an
.xcconfigfile with plaintext secrets), spawned anExplore-type subagent via theAgent/Tasktool with a broad research prompt that incidentally touched the denied file (among many other files). - The subagent's returned report included literal contents of the denied
.xcconfigfile — including plaintext credentials — confirmed by the repo owner to be the real values. The deny rule did not block this Read/Bash/Grep call. - Immediately after, the main agent's own
Read/Bashattempts on the same file were correctly blocked:"File is in a directory that is denied by your permission settings."/"Access to this file is blocked". - A fresh, minimal subagent spawned specifically to test
ReadandBash caton the exact same file was also correctly blocked, with the same error messages, same settings file (verified unchanged via mtime).
So: main agent blocked (both before and after), a second subagent blocked — but the first subagent's incidental access to the same file, under the identical settings, was not blocked.
Expected
permissions.deny (and the equivalent PreToolUse hook) should uniformly gate all tool calls in a session, including those issued internally by Task/Agent-tool subagents — no window where a subagent can read a denied path.
Actual
At least once, a Task-tool subagent successfully read a file that should have been denied per permissions.deny and a matching PreToolUse hook, while the identical rule correctly blocked the main agent and a later subagent test against the same exact file.
Impact
Real plaintext credentials from a denied file were exposed in the subagent's report — and therefore in the session transcript — despite an explicit deny rule intended to prevent exactly this. This is a security-relevant enforcement gap, not just a UX bug.
Environment
- Claude Code version: 2.1.198
- macOS 26.5.2 (build 25F84)
- Deny rule scope: user-level
~/.claude/settings.json; no project-level.claude/settings.jsonpresent
Notes
I could not find a deterministic repro — a second test subagent with an identical setup was blocked correctly. This looks like an intermittent enforcement gap specific to Task/Agent-tool subagent tool calls rather than a config mistake, but I can't rule out a more mundane cause (e.g. a hook-registration timing issue early in a session, before some watcher/cache attaches). Flagging regardless since the security impact — real secrets exposure — is confirmed.