CLAUDE LABJP
RUNNER — claude self-hosted-runner turns your own machines or containers into a place web, mobile, and desktop sessions can run, on Team and Enterprise plansPLUGIN — Plugins can now be installed from a zip over HTTPS, with no git or npm required and optional SHA-256 pinningBEDROCK — ANTHROPIC_BEDROCK_REGION_PREFIX lets Bedrock prefer a specific cross-region inference profile instead of the one derived from AWS_REGIONSTATUS — Sessions waiting on a sandbox, MCP input, or managed-settings prompt now read as Needs input rather than WorkingDLP — Inference Hooks are in beta for Enterprise. Prompts route to your own security server for an allow or deny verdict, typically within five seconds, before the model sees themWORKBENCH — The legacy Workbench and experimental prompt tools API retire on August 17, and Sonnet 5 promotional pricing runs through August 31RUNNER — claude self-hosted-runner turns your own machines or containers into a place web, mobile, and desktop sessions can run, on Team and Enterprise plansPLUGIN — Plugins can now be installed from a zip over HTTPS, with no git or npm required and optional SHA-256 pinningBEDROCK — ANTHROPIC_BEDROCK_REGION_PREFIX lets Bedrock prefer a specific cross-region inference profile instead of the one derived from AWS_REGIONSTATUS — Sessions waiting on a sandbox, MCP input, or managed-settings prompt now read as Needs input rather than WorkingDLP — Inference Hooks are in beta for Enterprise. Prompts route to your own security server for an allow or deny verdict, typically within five seconds, before the model sees themWORKBENCH — The legacy Workbench and experimental prompt tools API retire on August 17, and Sonnet 5 promotional pricing runs through August 31
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 Code212PreToolUse2subagents7permissions5

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 $10 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-07-27
Whose Environment Expands That Variable? Fingerprinting Your Effective Managed MCP Policy
Variable references in the Managed MCP allowlist and denylist now resolve from the startup environment and the managed-settings env block. I rebuilt both resolution orders locally to see where verdicts diverge, then wrote a preflight check that reduces the effective policy to a comparable fingerprint.
Claude Code2026-07-01
Don't Accept an Agent's Numbers and Citations As-Is — A Verification Gate Built on a Dedicated Auditor Subagent
A design that verifies every number and citation in an agent-generated summary using a separate subagent before accepting it — with working TypeScript for deterministic recomputation and fail-closed source matching.
📚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
See all →