Skip to content

Contributing ​

Thanks for your interest in contributing to Sensitive Canary!

Development Setup ​

bash
git clone https://github.com/coo-quack/sensitive-canary.git
cd sensitive-canary
pnpm install

Commands ​

bash
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 fixes

Name 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)
  1. Branch from develop: git checkout -b feat/your-feature develop
  2. Open a PR targeting develop
  3. After review and approval, merge into develop
  4. When ready to release, open a PR from develop → main

Hotfix ​

For urgent fixes that must go directly to production:

  1. Branch from main: git checkout -b hotfix/fix-description main
  2. Apply the fix and open a PR targeting main
  3. After review and approval, merge into main
  4. A backport PR to develop is created automatically by CI

If the backport PR has conflicts, resolve them manually before merging.

Adding a New Detection Rule ​

  1. Add the rule to src/lib/default-config.json — define id, description, regex, category, and optionally entropyThreshold and flags. src/lib/rules.ts reads that file; the checksum validators it calls by name live in src/lib/validators.ts, which is where a new one goes
  2. Add tests — cover true positives, false negatives, and entropy filtering
    • Add the rule to the EXAMPLES table in src/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_THING with the reason
    • If the pattern carries a *, + or {n,} on a character class, work out what input makes it backtrack and add that shape to no rule is quadratic in src/lib/__tests__/rules.test.ts. A pattern that does not return is a way past the hook, not a slow scan — see the note in docs/rules.md
    • The six postal-address rules (pii-address-cn to pii-address-es) are generated: edit scripts/address-rules.mjs and run node scripts/address-rules.mjs --write. src/lib/__tests__/address-rules.test.ts fails when the config and the generator disagree, so do not edit those six entries by hand.
  3. Update README.md — add to the detection rules table
  4. Update docs/rules.md — add full reference entry
  5. 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:

  1. Update version in package.json and .claude-plugin/plugin.json — the versions job 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
  2. Rename ## Unreleased in CHANGELOG.md to ## vX.Y.Z (YYYY-MM-DD)
    • docs/changelog.md is a symlink to CHANGELOG.md — do not edit it separately
    • This content is automatically used as the GitHub Release notes by release.yml
  3. Review docs/rules.md — add/update any changed rules
  4. Review README.md — update rule counts and tables if needed
  5. 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
  • Publishes to npm with trusted publishing (OIDC), so no npm token is involved
  • Creates a GitHub Release with notes extracted from CHANGELOG.md

The documentation site is also redeployed automatically on merge to main.

Deprecating a version ​

Trusted publishing covers npm publish only, so deprecation is done by a maintainer from their own machine, authenticating with 2FA:

bash
npm login --auth-type=web
npm deprecate "@coo-quack/sensitive-canary@X.Y.Z" "What is wrong with it and what to use instead"

An empty message undeprecates instead, so always pass one. There is no workflow for this: it would need a long-lived npm token in Actions secrets, which is what trusted publishing removed.

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:

bash
SENSITIVE_CANARY_INTEGRATION=1 pnpm test src/__tests__/pre-tool-use-hook.integration.test.ts

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

Test seams ​

SENSITIVE_CANARY_DEADLINE_MS shortens the hook's five-second window between files, so a test can pass it in a fraction of a second. The hook reads it only when VITEST is set, which vitest does in the processes it runs, and it is not a setting for users: it is not in the README. A value that is not a positive integer is ignored, and the window stays at five seconds.

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.

Released under the MIT License.