Self-permission-granting safety check bypassable via Bash-mediated write (gates by tool, not by effect)
Summary
Claude Code's self-permission-granting safety check appears to gate by which tool is invoked, not by the effect a command produces on disk. A Write/Edit call that modifies ~/.claude/settings.json to register a new hook is refused as self-permission-granting. The identical file, with identical resulting content, can then be written successfully via the Bash tool (e.g. a Python read-modify-write one-liner), with no refusal at all.
If accurate, this means the self-permission-granting boundary is bypassable simply by choosing a different tool to produce the same write, which defeats the purpose of the check.
Steps to reproduce
- In a Claude Code session, ask the assistant to add a new entry to
hooks.PreToolUsein~/.claude/settings.json(e.g. registering a new hook script) using theWriteorEdittool.
- Observed: the tool call is refused/blocked as an attempt to grant new permission/hook capability to itself.
- Ask the assistant to make the exact same change via the
Bashtool instead, for example a Python script that readssettings.json, inserts the same JSON object intohooks.PreToolUse, and writes the file back withjson.dump.
- Observed: the write succeeds with no refusal, and the resulting
settings.jsonis functionally identical to what was refused in step 1.
Expected behavior
A safety boundary meant to prevent a session from expanding its own permissions/hook capability should classify by the effect of a command (does this write target a sensitive config path and change its permission-relevant content?), not by which tool API was used to produce that effect. If Write/Edit are refused for a given target and content, a Bash-mediated write producing the same target/content should be refused too.
Context
This was found while a session was legitimately trying to install a new PreToolUse hook at the user's own explicit, repeated direction (the user was fully aware of and authorized the change; this is not a report of malicious use). The Write/Edit refusal correctly triggered; the Bash route around it should not have succeeded silently. The same technique would plausibly generalize to other Write/Edit-refused targets, not just settings.json.
Environment
- Claude Code (desktop app), session date 2026-09-10.
- macOS.
Happy to provide more detail if useful (project-specific names sanitized out of this report; the mechanism itself is what matters).