cerberus guards against an agent making a bad call: a destructive command it
didn't think through, a terraform destroy against the wrong workspace, a
force-push over someone else's commits. It is a seatbelt for an agent that
is trying to do the right thing.
It is not a sandbox, and it is not a boundary against an adversary who
already has shell access on the machine. Anyone or anything able to run
arbitrary commands can also edit config.toml, delete the rule scripts, or
replace the binary on $PATH. cerberus raises the cost of a mistake. It
does not contain an attacker.
Two specific limits worth knowing:
- Detection is best-effort.
rules::shell's tokenizer is quote-aware and splits on shell operators, but it is not a POSIX parser. A command obfuscated on purpose (base64, variable indirection,eval) can get past the pattern and rule checks. The checks are tuned to catch plausible mistakes, not deliberate evasion. - Heads fail open individually. A missing binary, a broken policy store, or a rule script that won't compile means that head allows. No single check can halt every Bash call by breaking.
The system compensates by failing closed as a whole. cerberus health runs
at every SessionStart and verifies that each enabled head is actually
enforcing, including a synthetic rm -rf / through cupcake and a synthetic
dangerouslyDisableSandbox: true through the rule scripts. If any check
fails it writes a sentinel, and cerberus gate then denies every Bash call
until cerberus init repairs things. A guard that has quietly stopped
working is treated as worse than no guard.
cerberus source add/sync (see Layered policy sources) clones
and runs a remote repo's .rhai/.rego content at the same trust level as
cerberus's own shipped rules — it executes against every guarded tool call,
the same as the content in this repo. A compromised or malicious source
repo is a real attack surface, not a hypothetical one: it can add an
always-allow rule, or a rule that exfiltrates data through its own logic.
Only add a source you'd trust to the same degree as cerberus itself. add
and sync --yes are both explicit, human-initiated actions specifically so
that never happens silently — a plain cerberus init/cerberus guard
never fetches anything, and sync without --yes only shows what would
change.
Before anything is installed, .rego content is run through opa check.
This is a syntax/compile gate, not a semantic one: it catches a source repo
that's broken or obviously malformed, and rejects the entire add/sync
if it fails — nothing is installed, config.toml isn't touched. It does
not catch a policy that parses cleanly but is deliberately malicious
(an intentional always-allow rule reads as perfectly valid Rego). If opa
isn't on $PATH, validation is skipped with a warning rather than blocking
the operation — cerberus doesn't require opa merely to accept a source,
only to enforce the policy head itself.
[audit] enabled = true (see Audit log) writes each denied/asked
call's raw tool_input — which can include a full Bash command line — to
${XDG_STATE_HOME:-$HOME/.local/state}/cerberus/cerberus.db in plaintext.
Anything typed inline into a guarded command (a bearer token, a database
URL with embedded credentials) ends up in that file. It's off by default
for exactly this reason, is never transmitted anywhere by cerberus itself,
and treating that file with the same care as shell history is the right
mental model.
Open a security advisory rather than a public issue.
Things worth reporting: a way to get a command past a shipped rule that the
rule plainly intends to catch, a way to make health report healthy while a
head isn't enforcing, or a way to clear the degraded sentinel without
repairing the underlying cause. Obfuscated-command bypasses are interesting
if the obfuscation is something an agent would plausibly produce on its own.
Only the latest release gets fixes. cerberus is pre-1.0 and there are no maintained release branches.