Skip to content

Releases: qkal/Canny

v0.3.0

Choose a tag to compare

@qkal qkal released this 22 Sep 23:24
f2c5e53

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 init again in each guarded project (or canny init --global) to add the new SessionStart hook. 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 test and true # npm test no longer count as a passing check. The first exits 0 when the tests fail, and the second runs none.
  • "allow": ["rules"] and ignore paths now keep edits from being sent to Jev, and both wait for canny trust, like every field that loosens the guard.

Faster sessions

  • A SessionStart hook tells the agent what the done-gate asks for and names the project's test command, read from package.json, a justfile, a Makefile, Cargo.toml, or go.mod.
  • A Bash check piped into tail (npm test 2>&1 | tail -20) is rewritten to run behind set -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 into tail.
  • Under pipefail, the last set -o or set +o pipefail in a command decides, and a check pattern in .canny.json such 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 leading cd that goes nowhere keeps a git-ignored .env writable.

Privacy

  • The Jev rule check can be turned off with "allow": ["rules"] and skips the paths in ignore, so part or all of a project's code can stay off TypeSafe's API without unsetting the key.
  • ignore patterns match a path however the agent spells it (./x, a/../x), for the done-gate as well.

Development

  • bench/run.mjs gives 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

Choose a tag to compare

@qkal qkal released this 21 Sep 17:26
a266600

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.ts is 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 .env is allowed again when the command opens with a cd that goes nowhere: cd "$PWD"; …, cd . && …, or a cd to the project's own absolute path. Agents open many commands that way, and any cd used to cancel the env-file exemption, so the write Canny's own message recommends was denied. Every other cd, pushd, or popd still 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, or tee that leaves fewer test cases, an append that adds .only or .skip, and a sed -i or perl -pi script whose s command 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 before s, and BSD sed -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.md or AGENTS.md after 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.ts was taken for scratch and its edit never reached the done-gate.
  • Rule text taken from CLAUDE.md and AGENTS.md is 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-ok is the one check to require.
  • pnpm check runs every gate, and pnpm install sets up a pre-commit hook that runs it, so a stale dist/ 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

Choose a tag to compare

@qkal qkal released this 21 Sep 01:20
1778c80

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 trust once per project that has a .canny.json. verify, ignore, rules, and allow are ignored until the file is trusted, because it lives in the repository the agent is editing. strict is still read from any config.
  • A check piped into tail no longer counts. pnpm test 2>&1 | tail -20 reports tail'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 with set -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 init and canny remove now 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 status no 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 as echo tsc or tsc --version.
  • > inside a quoted string or a heredoc body is not a file write, so git commit -m "a > b" no longer blocks a session that edited nothing.
  • cp, mv, rm, git rm, git checkout -- file, and git restore count 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.ts is denied like the same Write. A key that is only used, such as a curl header, is left alone.
  • Writing a key into a .env file that git ignores is allowed. An unignored .env, a symlinked one, and .env.example-style templates are still denied.
  • rm, git rm, and mv of 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 trust and canny remove are new. canny init writes hooks only for the agents it finds installed.
  • canny status and canny replay show the latest session of the project you are in, not of any project. canny sessions lists each session's project.
  • canny status prints 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 no canny on PATH, which is the case after the documented install.
  • init and remove recognise Canny's hooks by the statusMessage: "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 status prints.

Full changelog: https://github.com/qkal/canny/blob/v0.2.0/CHANGELOG.md · v0.1.0...v0.2.0