Contributing
Thanks for your interest in contributing to Sensitive Canary!
Development Setup
git clone https://github.com/coo-quack/sensitive-canary.git
cd sensitive-canary
pnpm installCommands
pnpm test # Run tests
pnpm run test:watch # Run tests in watch mode
pnpm run typecheck # Type check with tsc
pnpm run lint # Check with Biome
pnpm run fix # Lint + auto-fix with Biome
pnpm run ci # typecheck + lint + tests (full CI check)Branching Strategy
main
├── develop ← integration branch
│ └── <type>/* ← everything that is not an urgent production fix
└── hotfix/* ← urgent production fixesName the branch for the kind of change, using the same <type> vocabulary as the Conventional Commits requirement below: feat/, fix/, refactor/, test/, docs/, ci/, chore/. feature/ is accepted as a synonym of feat/. What matters is that the prefix says what sort of change it is, so that a list of open branches reads the way the commit history does.
Normal development
<type>/short-description → develop → main (release)- Branch from
develop:git checkout -b feat/your-feature develop - Open a PR targeting
develop - After review and approval, merge into
develop - When ready to release, open a PR from
develop→main
Hotfix
For urgent fixes that must go directly to production:
- Branch from
main:git checkout -b hotfix/fix-description main - Apply the fix and open a PR targeting
main - After review and approval, merge into
main - A backport PR to
developis created automatically by CI
If the backport PR has conflicts, resolve them manually before merging.
Adding a New Detection Rule
- Add the rule to
src/lib/default-config.json— defineid,description,regex,category, and optionallyentropyThresholdandflags.src/lib/rules.tsreads that file; the checksum validators it calls by name live insrc/lib/validators.ts, which is where a new one goes - Add tests — cover true positives, false negatives, and entropy filtering
- Add the rule to the
EXAMPLEStable insrc/lib/__tests__/rule-patterns.test.ts. Every rule needs one value it must find, or the id list proves only that a name is present: four rules shipped with patterns that could be disabled in silence because nothing asked them to match anything - The same file pads every example and requires the rule to still match, so a length you guessed too tight fails there rather than in a release. If the format really is exact — a checksum, a fixed-width field — add the id to
LONGER_IS_A_DIFFERENT_THINGwith the reason - If the pattern carries a
*,+or{n,}on a character class, work out what input makes it backtrack and add that shape tono rule is quadraticinsrc/lib/__tests__/rules.test.ts. A pattern that does not return is a way past the hook, not a slow scan — see the note indocs/rules.md
- Add the rule to the
- Update
README.md— add to the detection rules table - Update
docs/rules.md— add full reference entry - Update
CHANGELOG.md— add the rule under## Unreleased(see Changelog)
Changelog
Changes land under a ## Unreleased heading at the top of CHANGELOG.md, in the same ### Features / ### Fixes / ### CI / ### Documentation sections a released version uses. The release turns that heading into ## vX.Y.Z (YYYY-MM-DD) rather than writing the notes from scratch, so an entry is written by the PR that makes the change, while the reason for it is still at hand.
Release Checklist
When bumping a version, open a PR from develop → main with:
- Update
versioninpackage.jsonand.claude-plugin/plugin.json— theversionsjob in CI fails when the two disagree, or when either declares no version- Correcting a mismatch is the one version edit that does not need a release PR: the two files disagreeing is a bug, and CI is red until it is fixed
- Rename
## UnreleasedinCHANGELOG.mdto## vX.Y.Z (YYYY-MM-DD)docs/changelog.mdis a symlink toCHANGELOG.md— do not edit it separately- This content is automatically used as the GitHub Release notes by
release.yml
- Review
docs/rules.md— add/update any changed rules - Review
README.md— update rule counts and tables if needed - Run the integration test (see below) — CI never does, and it is the only check that the block still reaches Claude
After merging into main, release.yml automatically:
- Creates a git tag
vX.Y.Z - Creates a GitHub Release with notes extracted from
CHANGELOG.md
The documentation site is also redeployed automatically on merge to main.
Integration test
src/__tests__/pre-tool-use-hook.integration.test.ts runs the hook inside a real headless Claude Code session. It is the only test that checks Claude Code acts on what the hook says — the rest spawn the hook and read its output themselves, which passes whether or not the runtime reads that channel.
It needs credentials and network, so it is opt-in and CI skips it:
SENSITIVE_CANARY_INTEGRATION=1 pnpm test src/__tests__/pre-tool-use-hook.integration.test.tsRun it by hand whenever the way a block is returned changes — the exit code, the channel the reason is written to, or the shape of the payload.
Pull Requests
- Follow Conventional Commits (
feat:,fix:,docs:,hotfix:, etc.) - All tests must pass (
pnpm test) - Lint must pass (
pnpm run lint) - One approval required to merge
Code Style
Enforced by Biome. Run pnpm run fix before committing.