Repository navigation
v0.2.1
Canny 0.2.1 carries the test-removal and rule checks over to shell commands, closes two ways a key written from the shell got past the secret check, fixes one false deny, and adds a harness that measures Canny instead of asserting it.
Install or update as before: git -C ~/.canny/src pull --ff-only, or the prompt in the README. There is nothing to build, and nothing to do before updating.
Secrets
echo "const k = '<key>';" > src/k.tsis denied. A;inside the quoted text used to split the statement, so the secret check lost the redirect as soon as the written code ended in a semicolon.- The same goes for text with an escaped quote in it, such as
echo "a 5\" pipe; const k = '<key>'" > src/k.ts. Every shell reader here now treats\"inside double quotes as text. - Writing a key into a git-ignored
.envis allowed again when the command opens with acdthat goes nowhere:cd "$PWD"; …,cd . && …, or acdto the project's own absolute path. Agents open many commands that way, and anycdused to cancel the env-file exemption, so the write Canny's own message recommends was denied. Every othercd,pushd, orpopdstill leaves no env file exempt. The bench below found this one.
Tests and project rules
- A shell command that rewrites a test file gets the same ask as the edit would (deny on Codex): a heredoc,
echo, orteethat leaves fewer test cases, an append that adds.onlyor.skip, and ased -iorperl -piscript whosescommand or delete pattern takes cases out or puts skip markers in. - In-place edits are read the way the tools read them:
sed -E -i,--in-place,--expression=,perl -w -pi.bak, quoted paths, a;inside the script, an address befores, and BSDsed -i ''. Those files also reach the ledger now, so the done-gate sees them. - Text a shell command writes is checked against the rules in
CLAUDE.mdorAGENTS.mdafter it runs, like any other edit. As before, that can only add a note. - The script of an in-place edit is read, not run, so a delete by line number (
5,10d) is not seen. The README lists the rest of the ceiling. These checks are a speed bump for an honest agent, not a sandbox.
Smaller fixes
- In a project that itself lives under a temp directory, a file named like
..data.tswas taken for scratch and its edit never reached the done-gate. - Rule text taken from
CLAUDE.mdandAGENTS.mdis stripped of terminal escape sequences before it is printed or sent anywhere. - The Jev cache is written atomically, and a cache write that fails no longer throws away the judgment that was already fetched.
Measuring Canny
node bench/run.mjs --agent claude --runs 5 gives each task in bench/tasks to a headless agent in a scratch directory, once with Canny's hooks and once without, then puts the task's own tests back and runs them. Five tasks: a feature whose edge cases are only in the tests, a red suite after a refactor, a rename across four modules, a behaviour change where a test has to change, and an API key handed over in the prompt. Nothing is installed outside the scratch directory.
First numbers, five runs per task per arm with Claude Code 2.1.277:
| Model | Without Canny | With Canny | Mean time without / with |
|---|---|---|---|
| Opus 5 | 25/25 | 25/25 | 17.8 s / 22.3 s |
| Sonnet 5 | 25/25 | 25/25 | 20.0 s / 26.8 s |
On these tasks and these models Canny changed no outcome. It blocked once in nearly every run, because the agent verifies with npm test 2>&1 | tail, which does not count as a check, and then reran the tests on their own. That is the cost, measured; the benefit is still unmeasured.
For contributors
- CI runs as parallel jobs: static gates, tests on Node 22, 24, and 26 on Linux plus Node 24 on macOS, a smoke test of the README install, and CodeQL.
ci-okis the one check to require. pnpm checkruns every gate, andpnpm installsets up a pre-commit hook that runs it, so a staledist/is caught before the commit.test/fixtures/holds the hook payloads of a real Claude Code session and a real Codex session, scrubbed, with a snapshot of how Canny reads them.
Full changelog: https://github.com/qkal/Canny/blob/v0.2.1/CHANGELOG.md · v0.2.0...v0.2.1