Repository navigation
Releases: qkal/Canny
Release list
v0.3.0
Canny 0.3.0 stops costing the agent a turn. It tells the agent at session start what a finished session needs, and fixes the one habit that got nearly every session blocked, so a guarded run takes no longer than an unguarded one. It also closes two ways a failing test could pass the done-gate, and lets a project keep its code from being sent to Jev.
Install or update as before: git -C ~/.canny/src pull --ff-only, or the prompt in the README. There is nothing to build.
Before you update
- Run
canny initagain in each guarded project (orcanny init --global) to add the newSessionStarthook. Without it the rest of this release still works, but the agent learns the done-gate rule from its first blocked Stop instead of up front. ! npm testandtrue # npm testno longer count as a passing check. The first exits 0 when the tests fail, and the second runs none."allow": ["rules"]andignorepaths now keep edits from being sent to Jev, and both wait forcanny trust, like every field that loosens the guard.
Faster sessions
- A
SessionStarthook tells the agent what the done-gate asks for and names the project's test command, read frompackage.json, ajustfile, aMakefile,Cargo.toml, orgo.mod. - A Bash check piped into
tail(npm test 2>&1 | tail -20) is rewritten to run behindset -o pipefail, so it counts when it passes and reports its failure when it fails. On Claude Code the rewritten command still goes through your permission rules; Codex takes a rewrite only with an "allow" and asks for approval separately. - Measured with
bench/, Opus 5, 25 pairs: no blocked Stops where every earlier guarded run had one, 25 of 25 passes with and without Canny, the same turns and cost, and the guarded arm 1.6 ± 3.4 s faster, down from 4.5 s slower. No measurable overhead; also no measured change in outcome, since both arms pass every task.
The done-gate
- A negated check and a check named only in a comment no longer count, and a
#after an escaped space no longer hides a pipe intotail. - Under pipefail, the last
set -oorset +o pipefailin a command decides, and a check pattern in.canny.jsonsuch as^npm test$is matched against the command that feeds the pipe. - A shell command that rewrites a test file goes through the test-removal check, a
;inside quotes no longer splits a statement past the secret check, and a leadingcdthat goes nowhere keeps a git-ignored.envwritable.
Privacy
- The Jev rule check can be turned off with
"allow": ["rules"]and skips the paths inignore, so part or all of a project's code can stay off TypeSafe's API without unsetting the key. ignorepatterns match a path however the agent spells it (./x,a/../x), for the done-gate as well.
Development
bench/run.mjsgives five tasks to a headless Claude Code or Codex with and without Canny, judges them by hidden checks, and records turns, cost, and model time per run.- The test suite strips git's environment, so committing from a linked worktree no longer marks the main checkout bare. CI runs the tests on Node 22, 24, and 26 plus macOS, with CodeQL.
Full changelog: https://github.com/qkal/canny/blob/v0.3.0/CHANGELOG.md · v0.2.0...v0.3.0
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
v0.2.0
Canny 0.2.0 closes the ways an agent could pass the done-gate without a passing check, extends the secret and test-removal checks to shell commands, and makes the CLI report on itself honestly.
Install or update as before: git -C ~/.canny/src pull --ff-only, or the prompt in the README. There is nothing to build.
Before you update
- Run
canny trustonce per project that has a.canny.json.verify,ignore,rules, andalloware ignored until the file is trusted, because it lives in the repository the agent is editing.strictis still read from any config. - A check piped into
tailno longer counts.pnpm test 2>&1 | tail -20reportstail's exit status, so it proves nothing. Agents that pipe their test runs get blocked once and have to rerun the check on its own, or withset -o pipefail. - Tighten old ledgers:
chmod -R go= ~/.canny. New files are created owner-only; files from 0.1.0 keep their old mode. canny initandcanny removenow exit 1 when they refuse a settings file they cannot parse. They used to print the error and exit 0.- Ledgers written by 0.1.0 do not record their project, so
canny statusno longer picks them up on its own. They are still readable by file name.
The done-gate
- A check whose exit status never reaches the agent does not count: piped without
pipefail, backgrounded, followed by|| true, or followed by; echo done. Neither do commands that only print or inspect, such asecho tscortsc --version. >inside a quoted string or a heredoc body is not a file write, sogit commit -m "a > b"no longer blocks a session that edited nothing.cp,mv,rm,git rm,git checkout -- file, andgit restorecount as code changes.node_modules,.venv, caches, logs, and temp files outside the project do not.- One very long line of digits in command output stalled the hook for 41 s and lost the ledger entry. It now takes milliseconds.
Secrets and tests
- Shell commands that write a file go through the secret check:
echo "key = 'sk-…'" > src/config.tsis denied like the sameWrite. A key that is only used, such as acurlheader, is left alone. - Writing a key into a
.envfile that git ignores is allowed. An unignored.env, a symlinked one, and.env.example-style templates are still denied. rm,git rm, andmvof a test file or directory get the same ask as deleting it through an edit (deny on Codex). Renaming a test, or moving it into a directory, is not removal.
The README lists what the shell reading does not cover. These checks are a speed bump for an honest agent, not a sandbox.
The CLI
canny trustandcanny removeare new.canny initwrites hooks only for the agents it finds installed.canny statusandcanny replayshow the latest session of the project you are in, not of any project.canny sessionslists each session's project.canny statusprints how often the hook crashed and the last error. The hook fails open, so a crash used to be silent.- Messages name
node "<checkout>/dist/cli.js" …when there is nocannyon PATH, which is the case after the documented install. initandremoverecognise Canny's hooks by thestatusMessage: "Canny"marker, keep another tool's hook that shares a group with Canny's, refuse malformed settings files without touching them, and write atomically through symlinks.- Ledger files are owner-only, and escape sequences are stripped before anything enters the ledger, so command output cannot redraw what
canny statusprints.
Full changelog: https://github.com/qkal/canny/blob/v0.2.0/CHANGELOG.md · v0.1.0...v0.2.0