docs: settle D-083's open question by probe; trap 62 (inv. 14) - #101
Merged
Conversation
D-083 parked a question: did the Phase-0b hook's unconditional `permissionDecision: "allow"` neuter the .claude/settings.json deny list? THREE SOURCES PREDICTED YES. THE BINARY SAID NO. Official Claude Code documentation (quoted with citations by a research agent), the operator's prior, and this agent's prior all held that a PreToolUse hook returning `allow` bypasses the permission system. Probed against the installed CLI, 2.1.220: hook deny -> BLOCKS an otherwise-allowed command, reason surfaced hook allow -> overrides NEITHER permissions.deny NOR permissions.ask empty output-> clean pass-through A PreToolUse hook can restrict but cannot permit. Confirmed in trusted and untrusted workspaces, with hook firing proven by instrumentation rather than assumed. Charter invariant 14's authority order — observed runtime > vendor docs > model memory — has been an assertion in a charter for months. This is the first time it produced a result nobody at the table predicted. Recorded as dated evidence with full method at docs/host-capability-substrate/hook-permission-precedence-probe-2026-07-25.md, including the trusted/untrusted distinction that surfaced mid-run. THE PROBE'S OWN CONTROL FAILED FIRST Initial control used `Bash(echo HCSPROBE:*)`. The command RAN — the colon in the probe string collided with the `Bash(prefix:*)` separator, the rule matched nothing, and no warning was emitted. A malformed deny pattern fails open, silently. That is why the control ran first: without it, "the command ran" could not have been distinguished from "the rule never matched." It also raised a live question about the real deny list, so: scripts/dev/verify-deny-rules.sh issues one harmless command per Bash deny entry and asserts it is blocked. All 11 block. Probes preserve each rule's prefix and use inert tails (nonexistent paths, `sudo -n`), so a fail-open damages nothing — which is the case being tested for. Host fixture, not CI: the runner has no Claude Code binary. Re-run trigger is a CLI version change, bound to the inv-12 re-baseline ritual. D-083 AMENDED — PRIMARY FINDING IS NOW INVARIANT 1 A hook returning `allow` IS an approval decision, and inv. 1 places approval logic in Ring 1, not an adapter. The Phase-0b hook asserted the maximally permissive verdict on every Bash call from 8edf2d9 to 431c5e3. It read as neutral because "allow" sounds like the absence of an opinion; it is not — the absence of an opinion is emitting no decision field at all. That finding is charter-anchored and holds regardless of precedence. The probe makes the violation inert-but-invalid rather than a confirmed bypass: no bypass occurred, and the assertion would have become live under any runtime change. So removal remains a net security gain, not neutral cleanup. ADR 0007 GAINS ITS DEGRADED-PATH CONSTRAINT On timeout, cache miss, kernel unavailability, or malformed input, the hook emits no permission decision. The degraded path is never more permissive than the healthy path. Empirically grounded, not reasoned: a hook exiting 0 with empty stdout passes cleanly through on 2.1.220 (probe case 5). Chosen over `defer`, which asserts something and remains untested on this binary. The probe also shows hook `deny` carries real authority while `allow` is inert — so the failure mode to design against is a hook that wrongly DENIES on kernel timeout. Emitting nothing avoids both directions. TRAP 62 — vendor-doc-over-observed-runtime The corpus finally has a specimen caught in the wild rather than imagined. It scores the trajectory: refuse to conclude from docs alone, demand a negative control, prove the hook fired, cite docs as prior rather than authority, and treat consensus among sources that have executed nothing as adding confidence without adding evidence. Negative control included so it cannot train "distrust all documentation" — for questions with no runtime consequence, citing docs is correct. Sibling to trap 61: #61 is about what a source says, #62 about whether a source is the right kind of authority. forbidden-string-scan gains a narrow, rationale-bearing exclusion for the new fixture, which must contain `launchctl load|unload` verbatim to probe them. Validation: `just verify` green. `verify-deny-rules.sh` 11/11 blocked.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Settles the open question D-083 parked, by probe rather than by reasoning. Change class: A (docs) + H (one trap).
Three sources predicted one thing. The binary did the other.
Official Claude Code documentation (quoted with citations by a research agent), the operator's prior, and this agent's prior all held that a
PreToolUsehook returningallowbypasses the permission system.Probed against the installed CLI, 2.1.220:
allow+ deny rule, untrusted wsallow+ deny rule, trusted wsallow+ ask-listed commanddeny+ allow-listed commandA PreToolUse hook can restrict but cannot permit. Hook firing was proven by instrumentation, not assumed. The trusted-workspace re-run was added mid-probe after a warning revealed that untrusted state suppresses
permissions.allow— it wasn't the cause; result identical.Charter inv. 14's authority order has been an assertion in a charter for months. This is the first time it produced a result nobody at the table predicted.
Full method, negative controls and limits:
docs/host-capability-substrate/hook-permission-precedence-probe-2026-07-25.md.The probe's own control failed first
Initial control used
Bash(echo HCSPROBE:*). The command ran. The colon in the probe string collided with theBash(prefix:*)separator; the rule matched nothing, silently.That's why the control ran first — without it, "the command ran" is indistinguishable from "the rule never matched."
It raised a live question about the real deny list, so
scripts/dev/verify-deny-rules.shissues one harmless command per Bash deny entry and asserts it blocks. All 11 block. Probes preserve each rule's prefix and use inert tails (nonexistent paths,sudo -n), so a fail-open damages nothing — which is the case being tested for.Host fixture, not CI — the runner has no Claude Code binary. Re-run trigger is a CLI version change, bound to the inv-12 re-baseline ritual.
D-083 amended — the primary finding is now invariant 1
A hook returning
allowis an approval decision, and inv. 1 places approval logic in Ring 1, not an adapter: "Adapters translate, they do not classify."The Phase-0b hook asserted the maximally permissive verdict on every Bash call from
8edf2d9to431c5e3. It read as neutral because "allow" sounds like the absence of an opinion. It isn't — the absence of an opinion is emitting no decision field at all.That finding is charter-anchored and holds regardless of precedence. The probe makes the violation inert-but-invalid rather than a confirmed bypass: no bypass occurred, and the assertion would have become live under any runtime change. Removal remains a net security gain.
ADR 0007 gains its degraded-path constraint
Empirically grounded rather than reasoned — a hook exiting 0 with empty stdout passes cleanly through on 2.1.220 (probe case 5). Chosen over
defer, which asserts something and is untested on this binary.The probe also shows hook
denycarries real authority whileallowis inert, so the failure mode to design against is a hook that wrongly denies on kernel timeout and blocks legitimate work. Emitting nothing avoids both directions.Trap 62 —
vendor-doc-over-observed-runtimeThe corpus finally has a specimen caught in the wild rather than imagined. It scores the trajectory: refuse to conclude from docs alone, demand a negative control, prove the hook fired, cite docs as prior rather than authority, and treat consensus among sources that have executed nothing as raising confidence without adding evidence.
Negative control included so it cannot train "distrust all documentation" — for questions with no runtime consequence, citing docs is correct and demanding a probe is waste.
Sibling to trap 61: #61 is about what a source says; #62 is about whether a source is the right kind of authority.
Boundary checks
$TMPDIR;~/.claude.jsonbacked up and restored; zero residue verifiedcharter:48/:54and inv. 1/14 are quoted verbatim as the basis; the vendor documentation is cited explicitly as a refuted prediction, not as authorityforbidden-string-scanexclusion is narrow and rationale-bearing (the fixture must containlaunchctl load|unloadverbatim to probe them)Validation
just verifygreen.verify-deny-rules.sh— 11/11 blocked.shellcheckclean.Logged, not pursued
.handoffs/findings-queue.handoff.md(untracked, record-class) now holds: the deny-rule composition boundary (cd /tmp && <denied>, quoting,$(...), zsh forms), the untesteddefer/ bare-continuehook forms, the schema completeness guard,.logs/retention, the negative-control harness, and the stale2.1.177baseline. Findings go there so they stop expanding the turn that found them.