Guidance for AI coding agents (Claude Code, Gemini CLI, Codex, etc.) working in this repo. Humans should read README.md and docs/spec.md first.
vibecop is a Go 1.22+ CLI + background daemon + tview TUI that sits between a coding agent and its permission prompts and asks a second, context-free LLM whether each tool-use request looks safe. It is itself a security tool — the bar for correctness is higher than for typical glue code.
The full design lives in docs/spec.md. Read it before making non-trivial changes.
go build ./... # build all packages
go test ./... # run the whole suite
go test ./internal/<pkg>/... # focused
go vet ./... # static checks
go run ./cmd/vibecop -- <subcmd> # invoke the CLI without installingThere is no Makefile and no lint config beyond go vet. Keep it that way unless asked.
cmd/vibecop/main.go # binary entry point
cmd/*.go # one file per Cobra subcommand (start, hook, install, init, refine, …)
internal/
daemon/ # UDS server, JSON router, broadcast events
evaluator/ # LLM client (anthropic + openai formats), prompt resolution, init/refine
hooks/ # harness payload parsing, settings.json patching for install/uninstall
config/ # TOML loader, project hash (sha256 of abs path), storage paths
audit/ # rolling activity.jsonl + permanent daily audit logs
tui/ # tview app: activity feed, latency, log tail
telemetry/ # OTLP export (traces, metrics, logs); nil-safe
setup/ # interactive first-run wizard
attic/ # quarantined / experimental code; do not import
docs/spec.md # canonical design spec (long, authoritative)
docs/test-matrix.md # what is tested where
These are load-bearing. Spec sections in parentheses.
- Fail-open everywhere ("Failure Handling"). If anything in vibecop's own code fails — bad config, daemon down, parse error, LLM 500 — the hook MUST exit 0 so the user's coding agent is never blocked. The one exception: a successful
denyorescalateverdict from the LLM exits 1. - Three consecutive evaluator failures → suspended pass-through for the rest of the daemon's life (
cmd/start.go,maxConsecutiveFailures). Resume requiresvibecop test(or restart). Do not change this without updating spec + test (cmd/handler_test.go). - Per-harness JSON response contract ("Verdict → harness response contract" / "Per-harness JSON shapes" tables).
vibecop hookalways exits 0 — the harness keys off the JSON written to stdout.approveemits harness-native "allow" JSON;denyemits harness-native "deny" JSON plusVibeCop [DENY]: <reason>on stderr;escalateemits no JSON and lets the harness's normal flow run. Fail-open paths (parse error, daemon unreachable, unknown harness/event/verdict, marshal failure) emit no JSON and exit 0. The[DENY]/[ESCALATE]stderr line is gated on the JSON-shaping success path — never written when we couldn't actually emit the deny. - Project identity = SHA256 of absolute path (
config.ProjectHash). Do not hash the basename, do not normalize symlinks. Per-project storage at~/.vibecop/projects/<hash>/. - Activity log is ephemeral, audit log is permanent.
activity.jsonlis a rolling window of lastactivity_windowverdicts (default 10) used as LLM context.audit/YYYY-MM-DD.jsonlis the permanent record, only written whenaudit_enabled = true. Never read audit logs back into prompts. think: falsefor Ollama CoT models. Local endpoints with reasoning models (qwen3,deepseek-r1) need this in the request body to avoid 30s+ latencies. The injection lives ininternal/evaluator/.- Hook script must be a one-liner that delegates to
vibecop hook. Don't add logic to the installed shim. - No GUI, no Electron, single static binary. Reject any dep that drags in CGO or graphical frameworks.
- Telemetry is fail-open (
internal/telemetry). OTLP init failures, dropped events, or exporter timeouts MUST NOT block a permission check. All*telemetry.Providerhelpers are nil-safe — the disabled state is a nil receiver, not a feature flag everywhere. - Subsystem packages are domain-named, not protocol-named.
internal/telemetry/(notinternal/otlp/);internal/evaluator/(notinternal/llm/). The transport is an implementation detail of the subsystem.
- Cobra subcommands are one-per-file in
cmd/. New subcommand → new file → register ininit()withrootCmd.AddCommand. - Errors:
fmt.Errorf("context: %w", err)for wrapping. Daemon-internal errors go tolog.Printf; user-facing errors go toos.Stderr. - Config access:
VibeCopConfig()reads the loaded global. Don't re-read config inside request handlers. - Concurrency: the daemon uses
sync.Mutex(not channels) for the per-project store map and the failure counter. TUI subscribers receive events on buffered channels with adefault:drop case (silent backpressure — known limitation). - JSON IPC: newline-terminated. Use
json.NewEncoder(conn).Encode(...)andjson.NewDecoder(conn).Decode(...). Don't buffer. - Tests live next to code (
foo_test.go). Fakes are local types likefakeEvaluatorincmd/handler_test.go— no mocking framework. Keep it that way. - No new top-level dirs without spec update.
- Add a payload struct + parser in
internal/hooks/hooks.go(mirrorClaudeCodePayload/GeminiCLIPayload/CodexPayload/CopilotPayload). - Extend
DetectAndParse,parseWithFormat, anddefaultEventFor. - Add the harness's row to
WriteVerdictininternal/hooks/responder.gofor each(event, verdict)combination, plus table-test coverage inresponder_test.go. - Add subprocess invocation in
internal/evaluator/init.gofor the Guardian-prompt generation step. - Add idempotent settings-file patching in
internal/hooks/install.go. - Update spec.md, README.md, this file, and
cmd/install.go's--harnessenum. - Tests: payload parsing, responder rows, install/uninstall round-trip.
- Extend
Client.Evaluateininternal/evaluator/evaluator.gowith a newapi_formatbranch. - Build request + parse response symmetrically with the existing
openai/anthropiccases. - Honor
timeout_ms, recent-activity context, and the JSON-only verdict response contract. - Add a row to the recommended-models table in spec.md and README.md.
- Tests against captured fixture responses.
Preserve these — they were fixed in commit d839c76 ("code review: fix settings key loss, TUI freeze, think:false injection, baseline prompt gaps"):
settings.jsonpatching must not drop existing keys. Read → modify the relevant subtree → write the whole object back. Neveros.WriteFilea partial JSON.- TUI
app.QueueUpdateDrawfrom a goroutine is required; calling tview directly from the socket goroutine deadlocks. think: falsemust be on the request body for Ollama, not the system prompt.- Baseline prompt must enumerate denials/escalations explicitly — vague guidance produces inconsistent verdicts.
Before claiming a task done:
-
go test ./...is green -
go vet ./...is clean - If you touched the spec'd contract (exit codes, IPC, config schema), spec.md is updated
- If you touched README-visible UX, README.md is updated
- No new top-level deps without justification (
go mod tidyshows the diff) - Manual smoke:
vibecop testagainst your configured endpoint still succeeds
- Short comments only. The code should explain itself; reserve comments for non-obvious why.
- No
TODO:/FIXME:left behind unless tied to a tracked issue. - No emoji in code, commits, or stderr output.
- No premature abstraction. The codebase is small; flat is fine.
- New here? Read
docs/spec.mdend-to-end, then trace a single request fromcmd/hook.go→internal/daemon/daemon.go:handleConn→cmd/start.go:makePermissionHandler→internal/evaluator/evaluator.go:Evaluate→ back out to exit code. - Want to see what's tested?
docs/test-matrix.md. - Stuck?
git log --oneline -- <path>shows recent intent; commit messages are descriptive.