CLAUDE LABJP
2.1.273 — A round of connection work landed together: five opt-in headers for LLM gateways, and a notice when Claude Code stops trying to reconnect an MCP server09/29 — The date beside claude-sonnet-4-5 is 12 days out, but it is an earliest-possible estimate. The model is still Active, and public retirements get at least 60 days noticeMCP — People keep asking to reconnect a dropped server without ending the session. The disconnect is now announced, but reattaching is still something you do by handNEW — A scheduled task ran some days and not others. The cause was that only one folder had been bound to itWINDOWS — When Cowork fails on its very first task, check developer mode and the setup state before going looking for a causeHANDOFF — Before a long draft gets too heavy for one chat, decide on the three things the summary must carry into the next one2.1.273 — A round of connection work landed together: five opt-in headers for LLM gateways, and a notice when Claude Code stops trying to reconnect an MCP server09/29 — The date beside claude-sonnet-4-5 is 12 days out, but it is an earliest-possible estimate. The model is still Active, and public retirements get at least 60 days noticeMCP — People keep asking to reconnect a dropped server without ending the session. The disconnect is now announced, but reattaching is still something you do by handNEW — A scheduled task ran some days and not others. The cause was that only one folder had been bound to itWINDOWS — When Cowork fails on its very first task, check developer mode and the setup state before going looking for a causeHANDOFF — Before a long draft gets too heavy for one chat, decide on the three things the summary must carry into the next one
Articles/Claude Code
Claude Code/2026-08-07Advanced

The Write Your Permission Mode Never Stops — Auditing an Agent-Scoped PreToolUse Gate Across 150 Payloads

Workflow subagents auto-approve file edits regardless of your session permission mode. I measured the remaining boundary — an agent_id-scoped PreToolUse hook — across 150 payloads, found 54 silent pass-throughs, and rebuilt it to fail closed.

Claude Code253PreToolUse2subagents9permissions10

Premium Article

I stopped mid-edit while reviewing my automation settings.

Workflow subagents auto-approve file edits regardless of the session's permission mode. Reading that line, I realized I could not explain what was actually happening in my own setup. As an indie developer running a content generation pipeline across several sites, I assumed I had drawn a boundary around where agents may write. That boundary, it turned out, lived entirely on the permission-mode side.

Shell commands and MCP calls outside your allowlist can still prompt mid-run. File writes will not. Under auto, bypass, or claude -p, even the launch prompt is skipped.

So what is left? Following the docs, PreToolUse hook input carries agent_id and agent_type — but only when the hook fires inside a subagent. You can tell whose write this is. That is the remaining boundary.

I wrote the obvious gate, generated 150 payloads, and ran them through. The result was the opposite of what I expected.

54 Silent Pass-Throughs — Build the Population First

Before arguing about decision quality, you need to decide what range of input the decision has to survive. From the PreToolUse common input fields, I kept only the axes that actually change the branch.

  • permission_mode — six values: default, plan, acceptEdits, auto, dontAsk, bypassPermissions (the mode labeled Manual in the UI arrives as default)
  • Caller — main thread, two named subagents, a plugin-scoped my-plugin:reviewer, and an --agent launch that carries agent_type but no agent_id
  • Tool and target — inside the allowed root, outside it, under tests, a CI config file, and one input with file_path missing entirely

Six times five times five is 150. Here is the generator.

# payloads.py — the PreToolUse payload population
import copy
import itertools
 
BASE = {
    "session_id": "sess_gate_probe",
    "prompt_id": "550e8400-e29b-41d4-a716-446655440000",
    "transcript_path": "/home/u/.claude/projects/x/transcript.jsonl",
    "cwd": "/repo",
    "hook_event_name": "PreToolUse",
    "tool_use_id": "toolu_probe",
}
 
MODES = ["default", "plan", "acceptEdits", "auto", "dontAsk", "bypassPermissions"]
 
ORIGINS = [
    {},                                                # main thread
    {"agent_id": "ag_01", "agent_type": "doc-writer"},
    {"agent_id": "ag_02", "agent_type": "test-runner"},
    {"agent_id": "ag_03", "agent_type": "my-plugin:reviewer"},
    {"agent_type": "doc-writer"},                      # --agent launch, no agent_id
]
 
TOOLS = [
    ("Write", {"file_path": "/repo/content/a.mdx", "content": "x"}),
    ("Write", {"file_path": "/repo/src/config/pricing.ts", "content": "x"}),
    ("Edit",  {"file_path": "/repo/tests/t.spec.ts", "old_string": "a", "new_string": "b"}),
    ("Edit",  {"file_path": "/repo/.github/workflows/deploy.yml",
               "old_string": "a", "new_string": "b"}),
    ("Write", {"content": "x"}),                       # file_path missing
]
 
 
def build():
    out = []
    for mode, origin, (tool, tin) in itertools.product(MODES, ORIGINS, TOOLS):
        p = copy.deepcopy(BASE)
        p["permission_mode"] = mode
        p.update(origin)
        p["tool_name"] = tool
        p["tool_input"] = tin
        out.append(p)
    return out

The gate I fed them to is the one I wrote first. Written plainly, it looks like this.

# naive_gate.py — "just restrict subagent writes," first attempt
import json
import sys
 
ALLOWED_ROOTS = {
    "doc-writer": ["/repo/content"],
    "test-runner": ["/repo/tests"],
}
 
data = json.load(sys.stdin)
agent_type = data["agent_type"]
path = data["tool_input"]["file_path"]
 
roots = ALLOWED_ROOTS.get(agent_type, [])
if not any(path.startswith(r) for r in roots):
    print(f"{agent_type} may not write to {path}", file=sys.stderr)
    sys.exit(2)
sys.exit(0)

Running all 150:

OutcomeCount
BLOCK (exit 2, tool call cancelled)78
PASS (silent allow)18
PASS (hook errored, execution continued)54

Fifty-four of 150 — 36% — landed in a state where the hook crashed and the write proceeded anyway. Not blocked. Not allowed on purpose. The hook simply raised an exception, and nothing stopped.

Exit 1 Does Not Mean Failure — It Means "No Opinion"

The cause is in Claude Code's exit code contract.

For most hook events, only exit code 2 blocks the action. Exit code 1 is treated as a non-blocking error and execution proceeds, even though 1 is the conventional Unix failure code. The one exception is WorktreeCreate, where any non-zero exit aborts.

A Python script that dies on an uncaught exception exits 1. So a single KeyError quietly flips your gate from "stop this" to "I have no opinion."

Breaking down the 54, two distinct shapes appear.

CallerCauseCount
Main threadagent_type missing24
Main threadfile_path missing6
Subagentfile_path missing24

The 30 main-thread cases are fine to let through. agent_id is an optional field present only inside a subagent, so its absence is normal. But they passed by accident, not by design — an exception happened to behave the way I wanted.

The bottom row is the problem. Twenty-four writes originating from a subagent, where file_path could not be read, and the gate returned no judgment at all while the write went ahead. That was the moment I understood my boundary only applied to code paths that happened not to throw.

My prediction had been backwards. I worried the gate would be too strict and stall development. In practice, the inputs carrying the least information sailed straight through. A gate that cannot decide has to stop — otherwise it is decoration.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
A PreToolUse hook that exits 1 lets the write through. See the measured breakdown of how many of 150 payloads slipped past a naive gate
A complete, drop-in PreToolUse gate that scopes writes to per-agent_type roots, plus the settings.json wiring
Prefix matching quietly widens your allowlist: granting /repo/content also grants /repo/content-archive. Reproduction and fix included
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

Related Articles

Claude Code2026-06-12
Stopping Claude Code Before It Runs a Destructive Command — a PreToolUse Screening Hook
How I wired a PreToolUse hook that screens every Bash command Claude Code is about to run: the settings.json setup, the Python screening script, what exit code 2 really does, and the false positives I tuned out over several weeks.
Claude Code2026-09-04
The One File I Keep Out of Claude Code's Reach: project.pbxproj
With Xcode project files, the breakage that still opens costs far more than the breakage that refuses to open. Here is what I measured before moving every edit behind a script, and where the line sits today.
Claude Code2026-08-27
Trusting the allow rules in your repo, or moving them to the environment
On disposable machines, the permissions.allow rules committed to your repo are dropped while the workspace waits to be trusted. Here is what gets dropped and what survives on 2.1.246, where each kind of rule belongs, and a preflight that catches the gap before a run starts.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links