Your agent reads every file it can.
Leave one it should not.
canaryhook plants decoy credentials in your project, then blocks the tool call the moment an agent reads one or carries the value off the machine. No model, no signatures, no false positives by construction.
No API key · Nothing to train · 5 commands · Fails open · Open source · MIT
See it in action
Real terminal output, click to expand

canaryhook initPlant the decoys, wire the hook

the tripA credential hunt, denied and recorded
Two ways to trip it
Every tool call passes through the hook before it runs. Two checks, both plain string comparisons over the call's own input.
The call names a decoy
A Read with a file_path, a shell cat, an editor open, anything that mentions the path. Blocked before the tool runs, so the contents are never returned.
Read { "file_path": ".env.backup" }
→ denyThe call carries a canary
A shell argument, a fetch URL or body, a file write, a commit message. You never have to enumerate how a file can be read, only notice the value leaving.
Bash { "command": "curl -d key=AKIA…" }
→ denyWhat gets planted
Four files, each holding a unique, realistic, completely dead fake secret. Nothing in your code, build, lint or test path references them, which is what keeps false positives near zero.
.env.backupAWS-shaped access key and secretcredentials.json.bakStripe-shaped live secret keydeploy_key.oldOpenSSH private key blockdb_dump_2026.sqlAPI token inside an INSERT statementThe canary strings live in .canaryhook/canaries.json, which is watched too. Decoys are gitignored by default; pass --commit to track them.
How to use it
Install and plant
One command per project. init plants the decoys, merges a PreToolUse hook into .claude/settings.json alongside any hooks you already have, and gitignores the bait.
npm install -g canaryhook cd /your/project canaryhook init
Confirm it is armed
Checks that every decoy is still on disk and the hook is still wired. Useful after a teammate rewrites settings.json.
canaryhook status
Work normally
Nothing changes until something goes looking for credentials. When it does, the call is denied, the agent is told the file was a tripwire, and the event is appended to .canaryhook/alerts.jsonl.
{"at":"2026-10-15T06:19:38.156Z","reason":"canary-exfil",
"decoy":".env.backup","tool":"Bash",
"detail":"canary string from .env.backup appears in the tool call"}Gate it in CI
verify exits non-zero when any trip was recorded, so an agent-driven pipeline fails loudly instead of quietly shipping.
- run: npx canaryhook verify
Five commands
Every one takes --json.
canaryhook initPlant the decoys and register the PreToolUse hookcanaryhook statusAre the decoys planted, is the hook wiredcanaryhook alertsList every recorded tripcanaryhook verifyExit non-zero if anything tripped, for CIcanaryhook cleanRemove the decoys and unregister the hookFlags: --commit and --force on init, --keep-alerts on clean.
What it is actually for
Signature scanning fails against code that rewrites itself. canaryhook does not look at the attacker at all. The trap lives in the environment, and nothing can tell a decoy credential from a real one without reading it.
Prompt injection
A fetched page or GitHub issue body tells your agent to collect credentials and post them somewhere.
A hostile MCP server
A tool description that looks helpful, backed by code that sweeps the repo for secrets.
A dependency script
A postinstall that reads whatever it can reach during an ordinary npm install.
Your own agent
No attacker at all. It cats a credentials file into context, and that context lands in a transcript you push.
What it does not do
canaryhook detects, it does not prevent. An attacker that ignores the decoys is not caught, and a targeted one that reads only the real .env walks straight past.
A broad sweep such as grep -r AKIA . reads the decoys without naming them, so the read path misses it. It still trips later on the exfil path, when the canary string shows up in another call.
It is a smoke detector, not a lock. The value is that it costs nothing to run, needs no model, and stays quiet enough that you leave it installed.
Questions
Does this need an AI model?
No. canaryhook is a hook and a set of string comparisons. There is nothing to download, nothing to infer, and no API key. It adds no latency you can measure.
Will it fire on my own code?
It should never fire. The decoys are files no build, test, lint or runtime path has any reason to open, and they are gitignored by default. A trip means something went looking for credentials.
What happens when it trips?
The tool call is denied before it runs, so the agent never sees the file contents. The agent is told the file was a tripwire and not to retry, and the event is appended to .canaryhook/alerts.jsonl.
Can an attacker just read the real .env instead?
Yes, and that is the honest limit. canaryhook is a smoke detector, not a lock. It catches indiscriminate credential hunting, which is what injected instructions and hostile tools actually do.
Does it work with tools other than Claude Code?
The hook speaks the PreToolUse payload format, so anything that can shell out to a pre-tool check and read a deny decision can drive it. init currently wires .claude/settings.json for you.
Is it safe to run in CI?
Yes. canaryhook verify exits non-zero when any trip was recorded, and a malformed hook payload always exits 0, so a broken install fails open rather than wedging a session.