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 init
Expand
canaryhook init

Plant the decoys, wire the hook

the trip
Expand
the trip

A 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.

Read path

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" }
  → deny
Exfil path

The 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…" }
  → deny

What 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 secret
credentials.json.bakStripe-shaped live secret key
deploy_key.oldOpenSSH private key block
db_dump_2026.sqlAPI token inside an INSERT statement

The 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

1

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
2

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
3

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"}
4

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 hook
canaryhook statusAre the decoys planted, is the hook wired
canaryhook alertsList every recorded trip
canaryhook verifyExit non-zero if anything tripped, for CI
canaryhook cleanRemove the decoys and unregister the hook

Flags: --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.

Know the moment it happens.

One command. Four decoys. No model.

canaryhook

Honeyfiles and canary tokens for AI coding agents.

Commands

canaryhook initcanaryhook statuscanaryhook alertscanaryhook verify
Part of the aiagentflow suite

© 2026 aiagentflow. Built for AI-native developers