From 4c7379ee4dd01be7f2fdb30aef10b4f8788068c4 Mon Sep 17 00:00:00 2001 From: raj-khan Date: Fri, 23 Oct 2026 21:53:10 +0800 Subject: [PATCH] Fix verify passing where canaryhook was never initialized The decoys and .canaryhook/ are gitignored, so a CI checkout had no record and verify always reported no trips. It now fails when there is nothing to verify, and the README shows init before the agent step. --- README.md | 9 ++++++++- src/commands/verify.ts | 12 ++++++++++-- test/root.test.js | 13 +++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 877031c..5bec22c 100644 --- a/README.md +++ b/README.md @@ -126,8 +126,15 @@ with a planted payload on stdin, and tells you whether the deny came back. ## CI +For a job that runs a coding agent, plant the decoys before the agent step and +check the record after it. The decoys and `.canaryhook/` are gitignored, so +they only exist in a job that ran `init`; `verify` fails where canaryhook was +never initialized rather than reporting a clean record it cannot have. + ```yaml -- run: npx canaryhook verify +- run: npm install -g canaryhook && canaryhook init +# ... the step that runs the agent ... +- run: canaryhook verify ``` ## Limits diff --git a/src/commands/verify.ts b/src/commands/verify.ts index eb899ba..f1a4aaf 100644 --- a/src/commands/verify.ts +++ b/src/commands/verify.ts @@ -1,11 +1,19 @@ -import { readAlerts } from "../state.js"; +import { readAlerts, readState } from "../state.js"; /** CI gate: non-zero when anything went looking for the decoys. */ export function verify(root: string, options: { json: boolean }): number { + // Without a planted project there is nothing that could have tripped, and + // reporting "no trips" would be a gate that can never fail. + if (!readState(root)) { + const message = "canaryhook is not initialized here, so there is nothing to verify. Run canaryhook init first."; + if (options.json) process.stdout.write(`${JSON.stringify({ clean: false, initialized: false, trips: 0 })}\n`); + else process.stderr.write(`${message}\n`); + return 1; + } const entries = readAlerts(root); const clean = entries.length === 0; if (options.json) { - process.stdout.write(`${JSON.stringify({ clean, trips: entries.length })}\n`); + process.stdout.write(`${JSON.stringify({ clean, initialized: true, trips: entries.length })}\n`); } else if (clean) { process.stdout.write("No canary trips.\n"); } else { diff --git a/test/root.test.js b/test/root.test.js index 4bafe4d..f4ebe49 100644 --- a/test/root.test.js +++ b/test/root.test.js @@ -72,3 +72,16 @@ test("the output hook stays quiet on clean output", () => { assert.equal(run(["hook"], root, payload).stdout, ""); assert.equal(run(["verify"], root).status, 0); }); + +test("verify fails where canaryhook was never initialized", () => { + const empty = mkdtempSync(join(tmpdir(), "canaryhook-")); + const result = run(["verify"], empty); + assert.equal(result.status, 1); + assert.match(result.stderr, /not initialized/); + assert.equal(JSON.parse(run(["verify", "--json"], empty).stdout).initialized, false); +}); + +test("verify passes a fresh install with no trips", () => { + const { root } = project(); + assert.equal(run(["verify"], root).status, 0); +});