bun install --frozen-lockfile
bun run check # Biome, rustfmt, Clippy (-D warnings) and the full test suite
bun run coverage # cargo-llvm-cov; fails below 95% of lines or functions
bun run build # release layout in dist/ (dist/bin/cerberus)
bun run format # Biome and rustfmt, writing
cargo buildBun runs the project's scripts: package.json holds the commands, and the
TypeScript under scripts/ builds the release layout. Cerberus itself is
Rust and needs neither at runtime. packages/ is where a terminal UI would
live, as a Bun workspace, the way mitos lays out its own; it doesn't exist yet.
nix develop gives you the toolchain, Bun, Biome, cargo-llvm-cov, and
tirith, cupcake and opa, so the tests that exercise the real binaries run
instead of skipping.
All three binaries are vendored in nix/ as pinned upstream release binaries,
not taken from an upstream flake or from nixpkgs, so a build doesn't depend on
either one working. To bump one, change version in its file and replace the
per-platform hashes. Each upstream publishes a .sha256 next to every asset;
convert it to SRI with nix hash convert --hash-algo sha256 --to sri <hex>.
After a bump, run cerberus init and cerberus doctor against the new
binaries: the canaries are what prove cupcake and opa still enforce.
For the rule-script API, and how to add a rule, a policy or a tirith rule, see CONTRIBUTING.md.
This is a Cargo workspace with one crate, crates/cerberus. main.rs is a
single call; everything else is the cerberus library, so tests and the fuzz target
call the same code the binary does.
crates/cerberus/
rules/ policies/ templates/ # shipped content, embedded at build time
src/
cli/ argument parsing and one thin handler per command
service/ what each command does: guards, gates, healths, doctors, inits
domain/ plain types: Head, Check, hook events and verdicts
ports/ the three seams (below)
heads/ risk (tirith), policy (cupcake), judgement (rhai rules)
harnesses/ claude, codex, cursor, hermes, opencode
config/ Paths (every runtime location) and config.toml
state/ the decision database and per-session deny counts
sources/ layered policy sources
tests/ binary-level tests
fuzz/ cargo-fuzz target
docs/ these pages
nix/ package.nix, plus tirith.nix, cupcake.nix and opa.nix
Dependencies point inward: cli calls service; service uses heads,
harnesses, config, state and domain; nothing below service calls up
into it. A port exists only where there are several implementations or a real
test seam:
| Port | Implemented by | Why it's a seam |
|---|---|---|
HeadEvaluator |
risk, policy, judgement |
guard walks the enabled heads in order and stops at the first answer |
HarnessInstaller |
one per harness | init and doctor loop over harnesses instead of repeating a block for each |
Environment |
the real machine, and a fake in tests | health and doctor can be tested without tirith, cupcake or opa installed |
Unit tests sit beside the code. crates/cerberus/tests/commands.rs runs the
real binary against a scratch HOME and XDG_* tree with an empty PATH, so
it never depends on what's installed. Tests that need the real tirith, cupcake
or opa skip themselves when the binary is missing.
bun run coverage needs cargo-llvm-cov (cargo install cargo-llvm-cov --locked, or nix develop) and fails below 95% of lines or functions, the
same floor mitos holds. It writes coverage/rust.lcov for Codecov.
cargo-llvm-cov builds with cfg(coverage). The few tests that run a real
tirith, cupcake or opa are compiled out of that build
(#[cfg(not(coverage))]): on a runner without those tools they only skip
themselves, and a skipped body counts as uncovered. What they check is also
driven against stand-in tools in tests/commands.rs, and cargo test still
runs them.
The hook_event target feeds arbitrary bytes through everything cerberus does
with input it doesn't control: hook event parsing, decision JSON, Cursor's
payload translation and the shell tokenizer the rules use.
rustup toolchain install nightly
cargo +nightly install cargo-fuzz --locked
cd fuzz && cargo +nightly fuzz run hook_event -- -max_total_time=60cargo-fuzz needs nightly for its sanitizer instrumentation; the regular
toolchain stays on stable. The entry point is crates/cerberus/src/fuzz.rs,
compiled only under --cfg fuzzing.
GitHub Actions runs checks and coverage on pull requests to develop and
pushes to it. ClusterFuzzLite fuzzes for ten minutes on pull requests that
touch the crate, the fuzz target or its build files. CodeQL scans the Rust
sources, and the OpenSSF Scorecard workflow reports weekly. Dependabot opens
weekly update pull requests for Cargo and GitHub Actions.
Coverage uploads to Codecov only for same-repository runs, so forks never
receive the upload token. Maintainers need to add CODECOV_TOKEN as a
repository secret.