Help us shape the future of Go! Take the 2026 Go Developer Survey

cavet

module
v0.2.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 4, 2026 License: MIT

README

cavet

Release CI Go Reference

From caveat, "let him beware". A warning, not a prohibition.

Most of the code landing in repositories now is written by coding agents – sometimes eager to please the operator, and happy to skip the boring parts. At the same time, the agentic workflows doing the writing are themselves a rising attack surface. Put the two together and the direction is set: as agent-written code accelerates, the attack surface of the products being shipped has to shrink, not grow.

Security scanning already exists – most CI stacks run the same scanners cavet wraps – but it runs after the agent is done, and a finding that arrives after the change is complete arrives too late to shape it. Prose goes further: a paragraph in the system prompt, a section in the agent instructions, a skill. Any of these can tell an agent to be security-aware while it designs and builds. It works, in the sense that it sometimes fires. But prose is advice, not a guarantee: it does not behave the same way twice, and it evaporates between sessions.

And when CI does flag something, the findings land in a new agent session that has none of the context that produced them: what the previous session decided during design, why it decided it, and which of these findings are pre-existing baseline debt versus newly introduced by the change. The same false positive gets rediscovered and re-argued from first principles, session after session.

cavet is one operator's answer, in toolkit form: security at the same speed as everything else – operators and their agents producing more secure products, more often, with repeatable efficiency. The audit trail and the recorded baseline carry the context between sessions; the skills advise at the moments where judgement is actually needed; the CLI records every judgement – verdict, reason, actor – and is the only author of that record. The same checks, the same phases, the same output shape, the same audit trail, session after session. Nothing blocks. Everything advises. The agent or the operator chooses to remediate, defer, or dismiss. Teams that need enforcement build it themselves around these tools.

Status: v0.2.1. The CLI, the multi-arch engine image, installers for seven harnesses, the serve dashboard, and CI are all here and working; SPECIFICATION.md remains the design of record.

cavet staged scan finds a planted key, the finding is dismissed with a recorded reason, and the audit trail shows every event

Claude Code loads the cavet-triage skill, dispatches the cavet-security subagent, and reconciles the verdict for the operator

Network posture

Two properties, both structural rather than promised:

  • Scanning runs with the network off. The engine container is created with NetworkMode: none, enforced in internal/engineclient/client.go and pinned by CI tests; no scanner tier needs egress. Advisory database updates (cavet engine update-db) are fetched host-side and swapped into the engine's volumes; the engine container never gets network.
  • cavet lookup accepts identifiers only – CVE, GHSA and OSV ids, package coordinates, rule ids, CWE references. A command whose only parameters are identifiers structurally cannot carry a code snippet, a file path, or a secret.

What your agent does with your code afterwards is outside cavet's mandate.

How this differs from /security-review, Aikido and Semgrep Guardian

/security-review is prose plus a model reading the diff. Aikido's plugin and Semgrep Guardian are commercial scanners wired into the agent loop. cavet is deterministic scanners plus advisory skills, with advisory-database digests and verdicts recorded in a log the repository owns. docs/COMPARISON.md carries the full comparison: what each tool is, which scanners run, where code goes, what persists between sessions, what it costs.

Installation

Prerequisites
  1. Docker, with a running daemon – cavet init probes it first and tells you if it is unreachable. Image scanning additionally uses the docker CLI's buildx plugin (bundled with Docker Desktop) to build images.
  2. A cavet binary – the one-liner below installs it, or any channel in Advanced: other install channels.
  3. A git repository you want covered.
Install

One line per platform – checksum-verified download, binary on your PATH:

curl -fsSL https://raw.githubusercontent.com/ChaosChild/cavet/main/installers/binary.sh | bash

Installs the latest release for your OS/arch (darwin/linux × amd64/arm64) into ~/.local/bin – override with --dir <path> or CAVET_INSTALL_DIR, pin a version with bash -s -- --version <x.y.z>. Windows (pwsh, canonical two-step form – a piped script cannot bind param()):

irm https://raw.githubusercontent.com/ChaosChild/cavet/main/installers/binary.ps1 -OutFile binary.ps1
pwsh -NoProfile -File binary.ps1

Installs into $HOME\.local\bin and adds that directory to your user PATH (override with -InstallDir). Every download is verified against the release checksums.txt, and against the Sigstore signature too when cosign is installed. Other channels – manual download, go install, Homebrew, Scoop – are in Advanced: other install channels.

Upgrade

cavet update is the in-place upgrade path: it resolves the latest release, verifies it exactly like the installers (checksum, plus the Sigstore bundle when cosign is installed), and swaps the running binary at its real install location. That location is the reason it exists: re-running an installer defaults to ~/.local/bin (or $HOME\.local\bin) regardless of where cavet actually lives, so a Homebrew, Scoop, go install or manual install can end up shadowed by a stale copy. cavet update --check only reports whether an update exists. Development builds (go install, go build) refuse and point back at their own channel.

Engine image

Nothing to install by hand: cavet init pulls the engine image, ghcr.io/chaoschild/cavet-engine – public, multi-arch (linux/amd64, linux/arm64), and digest-pinned into .cavet/config.yaml on first run; cavet engine update-db pins the advisory databases there the same way.

  • Two variants: core (default – secrets, dependencies, SAST) and full (adds Trivy's Java vulnerability database); set engine.variant in config.yaml.
  • CAVET_ENGINE_IMAGE overrides the image reference entirely – local builds, mirrors, pinning.
  • Image scanning via scan.container_images: false (default), true (every Dockerfile* at the repository root), or an explicit list of Dockerfile paths, nested ones included (engine/Dockerfile). A list entry may also be a map with a build target: {dockerfile: engine/Dockerfile, target: final-core} – multi-stage Dockerfiles default to the last stage, and the stage you actually ship is usually the one worth scanning. cavet image add <path> --target <stage> writes that form. cavet image add/remove/list manages the list, so the file never needs hand-editing. cavet scan --image scans just the configured images: each one is built host-side via docker buildx build (build progress streams to your terminal), handed to the engine as a tar, and scanned offline by Trivy. A --full scan includes the image phase whenever images are configured, and a staged scan (including the pre-commit hook) includes it when one of the configured Dockerfiles is itself staged. Mind the pre-commit latency: the first build of a changed image takes minutes, and the phase fires only when the Dockerfile itself changed.

The image bundles Opengrep, Gitleaks, Trivy and Checkov. Trivy alone already covers dependencies, IaC misconfiguration and containers from one binary; Checkov is an optional, off-by-default second IaC scanner for operators who want its broader IaC rule set, enabled per repository with scanners.checkov: true in config.yaml. When enabled it joins every filesystem scan, staged scans and pre-commit hooks included, so expect the hook to get slower; checkov's own secret checks stay off, because secrets remain gitleaks' and trivy's job, and its findings arrive at medium severity (checkov's SARIF carries no per-check severity). The Opengrep rule corpus is LGPL-2.1 + Commons Clause: using cavet is fine, selling a service whose value derives from those rules is not – see the licence table.

Advisory databases stay fresh without rebuilding the engine. Trivy's databases ship baked into the image but live in volumes; cavet engine update-db swaps in the latest advisories as digest-verified OCI artifacts while the engine stays offline, and every scan records the vulnerability-database digest it ran with. cavet version and cavet engine status report each database's digest and age, scan output shows "advisory db: N days old" with tiered wording past the configured thresholds (engine.db.age-thresholds, defaults 5/10/14 days), and cavet doctor carries a report-only advisory-db section. Expect a one-time step-up of new findings after the first update on stale data: new advisories against unchanged code are new findings by design.

cavet init

Run it in the repository you want covered:

cd your-repo
cavet init             # add --hooks to also install the advisory pre-commit hook

It scaffolds .cavet/, pulls and starts the engine container, and runs a full baseline scan that records every pre-existing finding as debt (work through it later with cavet debt). The engine container is per-repository and long-lived: cavet restarts it when stopped and creates a new one only when absent. Repositories that get deleted or moved leave their containers behind; cavet engine prune removes those, and --all removes every cavet container except the current repository's.

Path Commit?
log/ yes – the append-only audit trail (.gitattributes sets merge=union on it)
config.yaml yes – engine variant + digest pin
design/ yes – design decisions
state/, cache/, reports/ no – derived; cavet rebuild regenerates them (state/db.json aside: per-host advisory bookkeeping written by cavet engine update-db)

The scaffolded .gitignore already excludes the derived directories. Never edit log/ by hand – the CLI is its only author.

Harness setup

Skills, any agent, one line. npx skills add ChaosChild/cavet installs the seven cavet skills into any of 70 supported agents (Claude Code, Codex, OpenCode, Pi, Hermes, zcode, Cursor, Copilot, Gemini CLI, Windsurf and more). The skills will walk you through installing the CLI and engine on first use: cavet-install prints the exact command, what it does, and waits for your explicit yes before running anything. Skills alone are enough to evaluate cavet and how it advises; the binary and engine are needed to actually scan.

The per-harness routes below copy the six phase skills and add what that line does not carry: the cavet-security subagent and the agent-instruction snippet, placed where the harness reads them. The binary from the install step above is still required on either path – the skills drive the CLI, they do not replace it.

Claude Code (plugin, no clone needed):

/plugin marketplace add ChaosChild/cavet
/plugin install cavet@cavet

Every other harness – codex, opencode, pi, hermes, zcode, deepseek – one line, no clone needed:

curl -fsSL https://raw.githubusercontent.com/ChaosChild/cavet/main/installers/fetch.sh | bash -s -- --harness codex

pwsh (canonical two-step form – a piped script cannot bind param()):

irm https://raw.githubusercontent.com/ChaosChild/cavet/main/installers/fetch.ps1 -OutFile fetch.ps1
pwsh -NoProfile -File fetch.ps1 -Harness codex

Or from a clone: bash installers/<harness>.sh / pwsh installers/<harness>.ps1 for any of the seven harnesses. What lands where, per harness: installers/README.md.

First scan
git add -A
cavet scan --staged

Exit codes are informational, never gating: 0 clean (or nothing staged), 1 findings present, 2 error. cavet --help lists everything; cavet describe --json emits the machine contract for tooling that wants it.

The trivy scan pass is bounded by scan.timeout in config.yaml (default 60m); on a very large workspace raise it, for one run with cavet scan --timeout 90m or permanently via the config key.

cavet serve
cavet serve             # add --port to change it (default 8765)

Opens the dashboard at http://127.0.0.1:8765: a posture strip by severity, findings with filters and pagination, per-finding verdicts and history, open items, and trend charts with hover read-outs. The bind address is 127.0.0.1, hard-coded: the dashboard has no authentication, so it must never be reachable off-host. Remote access is the operator's front door, not cavet's – an SSH tunnel (ssh -L 8765:127.0.0.1:8765) or an HTTPS proxy you trust, in front of the loopback listener. cavet itself never listens beyond loopback.

Data loads when the page opens and on the manual Refresh button; there is no polling. Every read goes against state/ as it exists on disk, so a scan or triage in another terminal shows up on the next Refresh. Charts read a precomputed metrics cache (state/metrics.json), rebuilt inside the scan and rebuild critical sections and recomputed at serve start when stale, so no request replays the log. The charts are inline SVG drawn in the browser from that cache; the page's only network dependency is the Google Fonts stylesheet, and everything else is served by cavet itself.

Advanced: other install channels
GitHub Releases manual download · go install · Homebrew · Scoop
GitHub Releases

Pick your OS/arch archive from https://github.com/ChaosChild/cavet/releases/latest, and download checksums.txt and checksums.txt.sigstore.json alongside it. cavet is a security tool: verify before trusting the download. Both the signature and the checksums are produced by the repo's own release workflow, so pin the certificate identity to it (no cosign yet? Install it):

cosign verify-blob \
  --bundle checksums.txt.sigstore.json \
  --certificate-identity-regexp '^https://github\.com/ChaosChild/cavet/\.github/workflows/release\.yml@' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  checksums.txt

sha256sum --check --ignore-missing checksums.txt   # macOS/Linux
# Windows: compare (Get-FileHash cavet_0.1.0_windows_amd64.zip).Hash against checksums.txt

Each archive holds the binary (cavet, or cavet.exe on Windows), LICENSE and README.md at its root – ./cavet runs straight from the extraction folder, no install step needed. To put it on your PATH:

tar -xzf cavet_0.1.0_linux_amd64.tar.gz   # or: unzip cavet_0.1.0_windows_amd64.zip
sudo install -m 0755 cavet /usr/local/bin/cavet
go install
go install github.com/ChaosChild/cavet/cmd/cavet@latest

The binary lands in $GOPATH/bin/cavet (or $GOBIN/cavet if set) – a directory Ubuntu does not put on your PATH by default. Fix:

echo 'export PATH="$PATH:$(go env GOPATH)/bin"' >> ~/.profile

then log out and back in.

Homebrew (macOS, Linux)
brew install --cask chaoschild/tap/cavet
Scoop (Windows)
scoop bucket add chaoschild https://github.com/ChaosChild/scoop-bucket
scoop install cavet

What it does

Command
init Scaffold .cavet/, start the engine, record existing debt as baseline
scan Run scanners for a scope (--staged, --diff, --full, --image) and fold the delta
image Manage the Dockerfiles cavet scans as container images: add, remove, list
finding Show one finding: row, locations, verdict
debt The pre-existing baseline, on demand only; shows undecided rows, --all for everything
triage Record a confirm or dismiss verdict with reason and confidence
suppress Silence a finding deliberately, with a reason
defer Acknowledge a finding, act later
log Read the audit trail, newest first; limit with --limit
items List open items: design concerns and verification requests
raise Open an item: a design concern or a verification request
resolve Close an open item with the decision or answer
lookup Advisory, package, and rule lookup – identifiers only, by design
engine Control the long-lived scanner container; prune removes containers whose repository is gone
rebaseline After a deliberate engine change: regenerate the baseline
rebuild Regenerate state/ from the log (the source of truth)
doctor replays the log and diffs state/ against it; doctor fix repairs log-derivable drift, refuses the rest
serve Dashboard on loopback: posture, findings, metrics (--port)
describe Machine contract for third-party installers
version cavet, engine, and advisory database versions: digests and ages, no container or network
update Update the cavet binary in place from GitHub releases, checksum and Sigstore verified

Judgement lives in the skills: cavet-design, cavet-design-review, cavet-secure-coding, cavet-triage, cavet-supply-chain, cavet-deployment, plus the cavet-install bootstrap skill that offers the binary install when a skill cannot find the CLI, and the cavet-security subagent that does focused review with nothing but Read and a cavet-only shell. The skills advise; the CLI is the only author of .cavet/ artefacts – every verdict, deferral, and suppression lands in the log with a reason and an actor.

Documentation

  • SPECIFICATION.md – what this is and why it is shaped this way. The design story, kept in the repo.
  • docs/COMPARISON.md – how cavet compares with /security-review, the Aikido plugin, Semgrep Guardian, Snyk, and the agent-security tools on a different axis.
  • Build and development documentation – implementation history, the spec annexes, the scanner spike, the distribution plan, install internals – lives at https://migatchev.co.za/projects/cavet.

Licence

cavet itself is MIT. See LICENSE.

The engine image is not uniformly MIT, and the difference matters if you intend to sell something built on this:

Component Licence
cavet CLI, skills, subagent, installers MIT
Gitleaks MIT
Trivy Apache-2.0
Checkov (optional, off by default) Apache-2.0
git (in the engine image) GPL-2.0
Opengrep engine LGPL-2.1
Opengrep rule corpus LGPL-2.1 + Commons Clause

The Opengrep rules are the semgrep-rules corpus, licensed by Semgrep, Inc. under LGPL-2.1 with a Commons Clause condition: you may not sell a product or service whose value derives entirely or substantially from them. Using cavet, distributing it, and building on it are all unaffected. Selling a hosted service whose value comes substantially from those rules is not.

The rules are only loaded by the deep scan tier. The fast tier – secrets and dependency scanning, which is what the pre-commit hook and staged scans use – is entirely MIT and Apache-2.0, and Opengrep can be disabled outright in config.yaml.

Directories

Path Synopsis
cmd
cavet command
Package main builds the cavet command: advisory security tooling for coding agents.
Package main builds the cavet command: advisory security tooling for coding agents.
internal
cli
Package cli wires the cavet command surface (cli-spec §5).
Package cli wires the cavet command surface (cli-spec §5).
config
Package config loads and validates .cavet/config.yaml (artefacts-spec.md §4).
Package config loads and validates .cavet/config.yaml (artefacts-spec.md §4).
describe
Package describe emits the machine contract for third-party installers: skill paths, subagent allowlists, trigger commands, engine digest, version metadata (cli-spec §12).
Package describe emits the machine contract for third-party installers: skill paths, subagent allowlists, trigger commands, engine digest, version metadata (cli-spec §12).
engineclient
Package engineclient owns the long-lived cavet-engine container for one repository: lifecycle, exec plumbing, report copy-out, and path translation (cli-spec §10).
Package engineclient owns the long-lived cavet-engine container for one repository: lifecycle, exec plumbing, report copy-out, and path translation (cli-spec §10).
events
Package events owns every shape in the cavet log: constants, payload structs, validating constructors, and the canonical encoding.
Package events owns every shape in the cavet log: constants, payload structs, validating constructors, and the canonical encoding.
fingerprint
Package fingerprint owns finding identity: rule keys, context normalisation, and hashing (SPECIFICATION.md §3.3, artefacts-spec.md §5).
Package fingerprint owns finding identity: rule keys, context normalisation, and hashing (SPECIFICATION.md §3.3, artefacts-spec.md §5).
lookup
Package lookup answers identifier-only queries against allowlisted advisory sources (spec §5.3).
Package lookup answers identifier-only queries against allowlisted advisory sources (spec §5.3).
output
Package output renders cavet's normative result blocks: markdown tables with an aggregate line and next-step hints, golden-tested against spec §4.1 (cli-spec §9).
Package output renders cavet's normative result blocks: markdown tables with an aggregate line and next-step hints, golden-tested against spec §4.1 (cli-spec §9).
projection
Package projection parses scanner SARIF into a finding model, normalises severities, and merges across scanners with pre-fingerprint secret collapse (cli-spec §§7–9; SPECIFICATION.md §3.3, §4).
Package projection parses scanner SARIF into a finding model, normalises severities, and merges across scanners with pre-fingerprint secret collapse (cli-spec §§7–9; SPECIFICATION.md §3.3, §4).
scan
Package scan orchestrates one scan: scope resolution and staging inside the engine container, scanner invocation contracts, SARIF projection, and the delta fold against state (cli-spec §§6–8).
Package scan orchestrates one scan: scope resolution and staging inside the engine container, scanner invocation contracts, SARIF projection, and the delta fold against state (cli-spec §§6–8).
serve
Package serve runs the cavet dashboard: a loopback-only HTTP server with the embedded page (converted from the approved mock) and JSON endpoints over .cavet state and the metrics cache (serve-task-1).
Package serve runs the cavet dashboard: a loopback-only HTTP server with the embedded page (converted from the approved mock) and JSON endpoints over .cavet state and the metrics cache (serve-task-1).
store
Package store owns the .cavet artefact directory: scaffolding, the repo lock, log append/read, replay, and atomic state writes (artefacts-spec.md §§1, 6, 7).
Package store owns the .cavet artefact directory: scaffolding, the repo lock, log append/read, replay, and atomic state writes (artefacts-spec.md §§1, 6, 7).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL