Installing wires four hooks into .claude/settings.json. From then on, shell
scripts run automatically in your session: one when a session starts, three
after every edit. That is the point of them, and it is also the thing to
understand before you install:
| Hook | Runs | Executes |
|---|---|---|
session-start.sh |
at session start | git log, ls, and the knowledge linter (python3) |
post-edit-lint.sh |
after editing a source file | your project's toolchain, see below |
migration-guard.sh |
after editing a *.up.sql |
python3, reading two files |
env-drift.sh |
after editing a config file | git diff, grep |
post-edit-lint.sh invokes the toolchain of the project you are editing:
cargo checkexecutesbuild.rs, which is arbitrary code from that repository;npx --no-install tscruns a binary from that project'snode_modules;go build/go vetcompile the package, which can invoke a C toolchain through cgo.
None of this is exotic. It is what your editor's language server already does, but it is worth stating plainly: editing a file in an untrusted repository runs that repository's build tooling. If you work in repositories you have not read, switch it off:
It lives in the repository. It arrives with a clone, and a pull request can change it. cairn therefore treats it as untrusted:
- every value that reaches a shell is validated against a pattern, and an out-of-pattern value falls back to the default with a warning on stderr;
- no configuration value is ever composed into a shell command. The knowledge-summary tool is chosen from a fixed set and invoked as an argument vector.
This is not hypothetical. An earlier version ran eval $kb_command with the
string taken straight from that file: arbitrary command execution, at session
start, with no prompt, from a file that arrives with git clone. It was found
by auditing this repository against its own security prompt, demonstrated with a
harmless touch, and removed. scripts/test-install.sh hostile-config now runs
a malicious config on every make verify and asserts that nothing executes and
the session still starts.
It scans for generic secret shapes: AWS, Google, npm and Stripe keys, JWTs, bearer headers, assigned secrets, private keys and blocks, connection strings, IP literals, secret-looking environment names, and forbidden filenames.
Your own names go in a deny file, not in the script. Copy
scripts/sanitize-deny.example.txt to .cairn/sanitize-deny.txt and add your
company, hosts, internal domains, issue prefixes and tenant ids, one
label|regex per line. Two reasons it works this way: you should not have to
edit a vendored scanner to add a name, and a deny list committed to a public
repository is a directory of exactly the names it exists to remove. This
repository learned that on itself: the scanner used to carry the source
projects' names inline, and publishing it would have announced every one of
them. If your names are sensitive, keep the deny file out of the repository and
point at it with --deny or CAIRN_DENY_FILE.
It is not a general-purpose secret scanner. It has no entropy analysis and no
maintained rule set behind it. For a repository that handles real credentials,
run it alongside a dedicated tool (gitleaks, trufflehog) rather than instead
of one. Its own coverage is asserted, since every pattern must fire on the fixture,
so what it claims to catch, it catches; it makes no claim about the rest.
--history scans every text blob in the git history, because a secret deleted
from the working tree is still in the history, and the history is what a clone
hands to a stranger. It skips fixtures/ and the scanner's own file, both of
which contain patterns and fake secrets by design, so a full sweep of your own
history for project names is a separate, deliberate run with your deny file.
Open a GitHub security advisory on the repository, or an issue if the problem is not exploitable. Please include the version (commit) and a reproduction. There is no bounty; there is a quick response and credit in the fix commit.
- No network calls. Nothing is sent anywhere; every hook is local.
- No telemetry.
- No credential handling of any kind.
- The installer writes only inside
--target, never overwrites an existing file without--force, and backs up.claude/settings.jsonto a timestamped name before merging.