Hold the refusal with tests, and make the document limits true - #5
Merged
Conversation
The refusal is the only hard enforcement in the product and had no test of its own: nothing in the suite ran guard-write.mjs. Cases 49-52 drive it the way Claude Code does, as a child process fed a PreToolUse event on stdin. Case 50 holds the property the refusal rests on — that spelling a path differently does not change the answer. It passes today because both sides of the comparison come from one string: the guard hands context() the target's own directory and compares the target against the workspace that call derived. That is a property of the call site, not of classify(), so a future caller passing a workspace from elsewhere would lose it silently. Hence the test. Case 52 asserts that a ticket the guard cannot parse leaves the write unrefused, and §10 now says so: the hook fails open where the server fails closed. A hook failing closed would make a single corrupt file in tickets/ freeze every repository in the project, including the documents the harness tells you to go and fix. The cost is that the refusal is not a guarantee, which is the claim §2 already makes about circumvention, reached from the other side. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011mjhmFa9c2dwxmNeH5tboy
456 lines, for a document that tells every task to read two documents of about 200. The rule was stated three times in a file that broke it. What went is duplication, not content. The reasoning behind six decisions was narrated here and recorded again in docs/adr/0002-0008, which is the drift the ADRs exist to prevent; each is now a sentence and a pointer. The tool table repeated what every tool already tells the client about itself, and §11 repeated product.md's list of non-goals — what stays there is the three exclusions that are architectural rather than product decisions. 456 to 338. Reaching 200 from here means dropping the stage table, the layout diagram or the refused-paths block, which are the parts most often read. Case 32 held two phrases this moved. The promise that nothing is deleted is the product's, so it is now asserted against product.md, where it is made. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011mjhmFa9c2dwxmNeH5tboy
Six places said both top-level documents were about 200 lines. One of them was 456, and the only test enforcing a bound checked the other document — which is how it got there. The two bounds now differ because the jobs do: product design at 200, since what a product is for and will not do is prose that grows only by repeating itself; architecture at 350, because how a system is built has reference material — a layout, a stage table, the exact paths a rule names — that a reader returns to and that sentences cannot replace. Case 53 holds both, and holds each document to stating the number it is held to, since a limit enforced in one place and stated in another drifts apart silently. Verified it fails: ten lines of padding, and it names the file, the count and the bound. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011mjhmFa9c2dwxmNeH5tboy
Case 50 asserted that a backslash-separated path is refused, which is true on Windows and wrong everywhere else: on Linux a backslash is an ordinary character in a filename, so that path names a different file and allowing it is correct. The case-insensitivity cases were already guarded; this one was not. Found by the ubuntu job on its first run against these tests, which is what the matrix is for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011mjhmFa9c2dwxmNeH5tboy
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.
Three commits, all on the same theme: the project says things about itself that nothing was checking.
The one refusal had no test. Nothing in the suite ran
guard-write.mjs— the only hard enforcement in the product, and the thing README and the architecture both lead with. Cases 49-52 drive it as Claude Code does, a child process fed aPreToolUseevent on stdin: the refusal itself, the documentation paths staying writable, and spelling a path differently not changing the answer.That last one passes today, and the test says why it can: both sides of the comparison come from one string, since the guard hands
context()the target's own directory. That is a property of the call site rather than ofclassify(), so a future caller passing a workspace from elsewhere would lose it silently.§10 now says which way the refusal fails. It fails open — a crashed, timed-out or confused hook lets the write through, as does a ticket it cannot parse. That is intended, for the same reason the server's direction is the opposite: a hook failing closed would let one corrupt file in
tickets/freeze every repository in the project. The cost is stated plainly, because a refusal that fails open is not a guarantee. Case 52 holds the behaviour.The document limits are now true. Six places said both top-level documents were about 200 lines;
architecture.mdwas 456, because the only test enforcing a bound checked the other file. It is now 345, cut by removing duplication rather than content — the reasoning behind six decisions was narrated here and recorded indocs/adr/0002-0008, which is the drift ADRs exist to prevent.Reaching 200 would mean dropping the stage table, the layout diagram or the refused-paths block, which are the parts most often read. So the bounds now differ and say so: product design 200, architecture 350, with case 53 holding both — and holding each document to stating the number it is held to, since a limit enforced in one place and stated in another drifts apart.
60/60 tests pass. Cases 52 and 53 were both verified to fail before being trusted.
🤖 Generated with Claude Code
https://claude.ai/code/session_011mjhmFa9c2dwxmNeH5tboy