Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 10 additions & 2 deletions src/commands/verify.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
13 changes: 13 additions & 0 deletions test/root.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Loading