Contributing to Guardrails¶
Thanks for helping make Claude's work better. Bug reports, ideas, docs and code are all welcome.
Ground rules¶
- Be kind. This project follows the Code of Conduct.
- For anything bigger than a small fix, open an issue first so we can agree on the approach.
- Security issues go through SECURITY.md, not public issues.
Development setup¶
You need Go (see go.mod for the version), golangci-lint
v2, and ideally gofumpt and
govulncheck.
git clone https://github.com/rossedman/guardrails
cd guardrails
make test # go test -race ./...
make lint # golangci-lint, one command per file, doc comments
make cover # the 90% coverage gate on hand-written code
make check # everything CI checks, before you push
make gate # build the binary and run its own gate on itself
make harden # mutation-test and fuzz what this branch changed
make bench # benchmarks into bench.txt, for benchstat
make fuzz # every fuzz target for FUZZTIME (default 30s)
To try your build inside Claude Code, point the plugin launcher at it:
Then add your checkout as a local marketplace and install from it:
Scanner feedback with sarifix¶
sarifix.yaml configures sarifix
with golangci-lint and gitleaks. Run sarifix check to see the findings
your uncommitted changes introduce. With the
sarifix plugin
installed in Claude Code, the same check runs before Claude finishes a turn.
The sarifix workflow runs it on pull requests, and on main uploads to
code scanning.
The bar¶
Guardrails holds itself to its own gate, and CI enforces it:
go test -race ./...passes on Linux and macOS.- Total coverage stays at or above 90%, with no package below 80%.
golangci-lint runis clean. No//nolintwithout a specific linter and a reason.- Code is gofumpt-formatted.
- Every function and method, exported or not, has a doc comment starting
with its name, and every package has a package comment
(
go run ./tools/doccheck ./...). - Each file in
internal/clidefines at most one cobra command (tools/check-cmds.sh). - Every critical path below has a benchmark, and every one that takes outside input has a fuzz target that runs 30s clean in CI.
Tests should assert behavior, not just execute code; make harden
mutation-tests your changes to prove it. Prefer table-driven
tests. Hooks are pure functions from stdin JSON to stdout JSON; the engine's
golden files live in internal/engine/testdata and are regenerated with
go test ./internal/engine -update.
Critical paths¶
These functions parse input Guardrails doesn't control, run in hot loops,
or resolve configuration. Each has a Benchmark and, where it takes outside
input, a Fuzz target that asserts properties (determinism, invariants),
not just "didn't crash". Add to this list when you add code like this, and
commit any failing fuzz input under the package's testdata/fuzz/.
| Path | Why | Bench | Fuzz |
|---|---|---|---|
hook.ReadInput, Input.Tool |
every hook event's JSON from Claude Code | yes | yes |
guard.CheckBash, guard.Segments |
every Bash command, before it runs | yes | yes |
guard.CheckFile, guard.IsProtected |
every edit, before it lands | yes | yes |
lessons.Detect |
every prompt the user sends | yes | yes |
glob.Match, glob.Any |
every file against every pattern | yes | yes |
deps.FromCommand |
every Bash command, for package installs | yes | yes |
deps.FromManifest |
every manifest edit | yes | yes |
deps.Suggest |
typosquat check against the popular-package lists | yes | yes |
config.Merge, config.Load |
config resolution from user and repo files | yes | Merge |
coverage.Parse |
every Go coverage profile | yes | yes |
diff.ParseUnified |
every git diff the gate reads |
yes | yes |
docker.Parse |
every Dockerfile edited or gated | yes | yes |
mypy output parsing (python) |
every mypy run | yes | yes |
feedback.Render |
every gate report shown to Claude | yes | yes |
Adding a language module¶
- Create
internal/module/<lang>implementingmodule.Module(Match,Fix,Check,Gate,Tools). - Register it in
engine.New. - Add its config block to
internal/configandschema.json(the config tests fail if the schema and defaults drift apart). - Add tests with
runner.Fake, plus a real-tool integration test that skips when the tool is missing. - Document it in
docs/and add a rule file underinternal/assets/rules/.
Pull requests¶
- Every change to
maingoes through a pull request. It needs an approval from a code owner (.github/CODEOWNERS), every CI check green on an up-to-date branch, and all review conversations resolved. - Pull requests are squash-merged, so the PR title becomes the commit message: write it as a Conventional Commit.
- The branch and tag rules live in
.github/rulesets/and are reviewed like code. When you change them, a maintainer applies them withgh api -X PUT repos/rossedman/guardrails/rulesets/<id> --input .github/rulesets/<file>.json. Add new CI jobs that run on every pull request to the required checks inmain.json. - Questions and ideas go in a GitHub issue.
Commit messages¶
Use Conventional Commits:
feat:, fix:, docs:, test:, build:, ci:, chore:, for example
fix(go): test packages that depend on the change.
Release notes are generated from them.
Releasing¶
Maintainers:
- Set the version everywhere it's pinned (the plugin manifest, which the
launcher uses to pick the binary, the install snippets in the README and
docs, and the changelog's Unreleased heading), then open a PR:
make version VERSION=X.Y.Z. - Merge to
main, then tag the merge commit with the same version:git tag vX.Y.Z && git push origin vX.Y.Z. - The release workflow verifies the plugin version matches the tag and runs GoReleaser. A tag pushed without step 1 fails that check; bump, merge, and tag again.
Regenerating the graphics¶
The README header, social preview and terminal image are HTML pages in
docs/assets/src/, styled with the design tokens in brand.css (the site
uses the same tokens in docs/assets/site/landing.css). Render them to PNG
with headless Chrome or Chromium: