juliusbrussee/caveman
tldr.page
CLAUDE

CLAUDE.md — caveman

README is a product artifact

README = product front door. Non-technical people read it to decide if caveman worth install. Treat like UI copy.

Rules for any README change:

  • Readable by non-AI-agent users. If you write "SessionStart hook injects system context," invisible to most — translate it.
  • Keep Before/After examples first. That the pitch.
  • Install table always complete + accurate. One broken install command costs real user.
  • What You Get table must sync with actual code. Feature ships or removed → update table.
  • Preserve voice. Caveman speak in README on purpose. "Brain still big." "Cost go down forever." "One rock. That it." — intentional brand. Don't normalize.
  • Benchmark numbers from real runs in benchmarks/ and evals/. Never invent or round. Re-run if doubt.
  • Adding new agent to install table → add detail block in <details> section below.
  • Readability check before any README commit: would non-programmer understand + install within 60 seconds?

Project overview

Caveman makes AI coding agents respond in compressed caveman-style prose while preserving technical substance, code, commands, and exact errors. Publish no reduction or quality-equivalence percentage without a committed reviewed benchmark. Ships as Claude Code plugin, Codex plugin, Gemini CLI extension, and agent rule files for Cursor, Windsurf, Cline, Copilot, and other profiles via npx skills.

Repository routing

This repo is source of truth for Caveman skills, Engine, and MV3 directive extension. Agent SDK + initializer work belongs in /Users/julb/Desktop/GitHub/caveman-agent-sdk (JuliusBrussee/agent-sdk). Proprietary Pebble runtime, policy, sessions, TUI, distribution, and conformance work belongs in /Users/julb/Desktop/GitHub/caveman-coding-agent (intended JuliusBrussee/caveman-coding-agent). Browse driver/MCP/benchmark/plugin work belongs in /Users/julb/Desktop/GitHub/caveman-browse (JuliusBrussee/caveman-browse). The matching browse/ directory here is a consumer copy (it still builds the caveman-browse release binary); edit only for pinned integration, migration/removal, or an explicitly requested cross-repo sync.

Visibility is separate from ownership: this repo and caveman-browse are public now; caveman-agent-sdk is private during development and planned for public release after its release gates; caveman-coding-agent and Caveman-Cloud remain private commercial source.


What lives where

Post-cleanup layout. Sources of truth at the top, distribution mirrors below, build outputs in dist/, human docs alongside each skill.

caveman/
├── README.md                    # Front door (product pitch)
├── INSTALL.md                   # Per-agent install commands
├── CONTRIBUTING.md              # Dev guide
├── CLAUDE.md                    # This file (maintainer instructions)
├── AGENTS.md / GEMINI.md        # Autodiscovery files (must stay at root)
│
├── install.sh / install.ps1     # 30-line shims → installer/install.js
│
├── installer/                   # Unified installer
│   ├── install.js               # Single source for all 30+ agents (PROVIDERS array)
│   └── lib/settings.js          # JSONC-tolerant settings.json reader/writer
│
├── skills/                      # ALL skills, single source of truth
│   ├── caveman/{SKILL.md, README.md}
│   ├── caveman-commit/{SKILL.md, README.md}
│   ├── caveman-review/{SKILL.md, README.md}
│   ├── caveman-help/{SKILL.md, README.md}
│   ├── caveman-stats/{SKILL.md, README.md}
│   ├── caveman-compress/{SKILL.md, README.md, scripts/}
│   └── cavecrew/{SKILL.md, README.md}
│
├── agents/                      # cavecrew subagents (single source — kept at root for plugin auto-discovery)
│   └── docs/                    # profile-registry docs — NOT agents (see note below)
├── commands/                    # Codex/Gemini TOML command stubs (root for plugin auto-discovery)
│
├── src/                         # Internal source — not auto-discovered by plugin
│   ├── hooks/                   # Claude Code hooks (installer reads here)
│   ├── rules/                   # Auto-activation rule body (single source)
│   ├── tools/                   # caveman-init.js (per-repo rule writer)
│   └── mcp-servers/             # caveman-shrink npm-published MCP middleware
│
├── packages/                    # Current public packages
│   ├── cli/                     # @caveman-ai/cli
│   ├── pi-extension/            # @caveman-ai/pi — native Pi extension (bundled into the CLI, published on `pi-v*` tags)
│   ├── sdk/                     # TypeScript + Python gateway clients
│   ├── subagent-tax/            # Local harness-prefix benchmark
│   └── shared/                  # Contracts + binary installer
├── engine/ · proxy/             # Local compression runtime + provider proxy
├── mcp/ · mem/ · shrink/        # Recovery tools, memory, output compression
├── browse/                      # consumer copy; source = caveman-browse
├── extension/                   # MV3 extension source
├── shared/                       # Provider catalog + platform libraries
│
├── .claude-plugin/              # Claude Code plugin manifest (REQUIRED at root)
├── .cursor-plugin/plugin.json   # Cursor plugin: skills/, hooks/hooks-cursor.json, agents/ (default scan)
├── hooks/hooks-cursor.json      # Cursor plugin sessionStart hook ONLY. Never add hooks/hooks.json:
│                                #   Claude Code and Gemini auto-load it from the plugin/extension root
├── plugins/caveman/             # Claude Code plugin distribution (CI-mirrored)
│   ├── skills/                  # ← from skills/
│   └── agents/                  # ← from agents/
│
├── dist/                        # Build artifacts (gitignored)
│   └── caveman.skill            # ZIP of skills/caveman/, rebuilt by CI
│
├── tests/                       # All tests (Node + Python)
├── benchmarks/                  # Real token measurements through Claude API
├── evals/                       # Three-arm eval harness
├── docs/                        # User-facing docs site
└── .github/workflows/           # CI sync

File structure and what owns what

Single source of truth files — edit only these

FileWhat it controls
skills/caveman/SKILL.mdThe caveman voice: rules, auto-clarity, persistence. Sibling skills skills/ultracave/SKILL.md (grammar stripped) and skills/megacave/SKILL.md (文言文) are self-contained and each map 1:1 to a stored mode. Edit these three for behavior changes.
src/rules/caveman-activate.mdAlways-on auto-activation rule body, consumed by src/tools/caveman-init.js (per-repo IDE rule files) and by the opencode AGENTS.md block. GENERATED: skills/compile.mjs derives it from skills/caveman/SKILL.md (thesis line + rule headlines) plus the fixed tail in skills/activation-rule.mjs, and rewrites the RULE_BODY fallback in caveman-init.js to match. Edit the skill or the tail, rerun node packages/cli/scripts/compile-registries.mjs; tests/installer/rule-copies.test.mjs fails on drift.
src/rules/caveman-openclaw-bootstrap.mdMarker-fenced bootstrap snippet appended to ~/.openclaw/workspace/SOUL.md by installer/lib/openclaw.js. Drives always-on caveman through the OpenClaw gateway. Must include the SENTINEL Respond terse like smart caveman and stay well under OpenClaw's 12K-per-bootstrap-file cap.
.codex/codex-sessionstart.jsRepo-local Codex SessionStart hook. Resolves the configured default mode through src/hooks/caveman-config.js and emits the active skill's ruleset whole, replacing the hardcoded full-level echo .codex/hooks.json used to carry — so CAVEMAN_DEFAULT_MODE, a repo-local .caveman.json and a user-config defaultMode: "off" all take effect on Codex. Also the user-level always-on hook (#573): installer/install.js --only codex copies it (shipped via package.json files) into the owned payload $CODEX_HOME/caveman/hooks/ beside caveman-config.js and package.json, with caveman/skills/{caveman,ultracave,megacave}/SKILL.md, and merges one SessionStart entry into $CODEX_HOME/hooks.json, identified by that script path so foreign and caveman-CLI native-hook entries are never touched. --no-hooks/--minimal skip it; uninstall unmerges before removing the payload. Codex asks the user to trust a new hook via /hooks; never pre-trust it. Carries a hand-copied FALLBACK_VALID_MODES for when the shared resolver is absent; tests/hooks/codex-sessionstart.test.mjs fails if it drifts from VALID_MODES.
installer/lib/openclaw.jsOpenClaw install/uninstall helper. Frontmatter merge (version, always: true), SOUL.md marker append/strip, idempotent. Shared by installer/install.js and src/tools/caveman-init.js.
skills/caveman-commit/SKILL.mdCaveman commit message behavior. Fully independent skill.
skills/caveman-review/SKILL.mdCaveman code review behavior. Fully independent skill.
skills/caveman-help/SKILL.mdQuick-reference card. One-shot display, not a persistent mode.
skills/caveman-compress/SKILL.mdCompress sub-skill behavior.
skills/cavecrew/SKILL.mdCavecrew decision guide — when to delegate to caveman subagents vs vanilla. Edit only here.
skills/{caveman-setup,caveman-discover,caveman-learn,caveman-manage,caveman-optimize,caveman-explore,caveman-evidence-review}/SKILL.mdEngine/proxy driver skills. Ship with the plugin (see the auto-discovery note below).
skills/{investigate-first,lean-build,surgical-patch,safe-refactor,migration,verify-and-stop}/SKILL.mdToken-discipline work patterns — same goal as caveman prose (fewer output tokens) applied to code volume rather than wording. Deliberately un-branded so they read as generic patterns to the model. Ship with the plugin.
agents/cavecrew-investigator.mdRead-only locator subagent (haiku). Output contract: path:line — symbol — note.
agents/cavecrew-builder.mdSurgical 1-2 file editor subagent. Refuses 3+ file scope.
agents/cavecrew-reviewer.mdDiff/file reviewer subagent (haiku). One-line findings with severity emoji.
src/plugins/opencode/plugin.jsopencode native plugin. ESM Bun module — session.created writes flag, tui.prompt.append parses slash/natural-language activation and appends per-prompt reinforcement. Reuses caveman-config.js via createRequire.
src/plugins/opencode/commands/*.mdSix opencode slash-command prompt templates (/caveman, /caveman-{commit,review,compress,stats,help}).

skills/ is auto-discovered wholesale — every subdirectory ships

.claude-plugin/marketplace.json sets "source": "./", so the plugin root IS the repo root, and Claude Code auto-discovers every skills/*/SKILL.md with no skills key in plugin.json to gate it. Adding a directory under skills/ therefore installs it into every plugin user's agent, where its description competes for activation on ordinary work.

There is no allowlist. Before adding a directory here, decide whether it should reach end users; if not, it belongs under packages/ or another root. Keep the two tables above and the README "What you get" table in sync with the actual directory listing — ls skills/ is the source of truth.

agents/*.md is auto-discovered too — and plugin.json must NOT list them

Same mechanism, sharper edge. plugin.json used to carry an explicit "agents": ["./agents/cavecrew-*.md"] array. On Claude Code 2.1.235 that array loads zero agents — claude plugin details caveman reports Agents (0) with it and Agents (3) without it, so the three cavecrew subagents the cavecrew skill delegates to did not exist for any plugin user. The docs say string | string[] is valid and a directory string is rejected outright (agents: Invalid input), so the default agents/ scan is the only path that works. Do not re-add the key.

Consequence: every .md anywhere in agents/ becomes a subagent, named from its frontmatter or filename. Maintainer documentation belongs outside that tree; profile-registry orientation lives in docs/technical/agent-profile-registry.md. tests/verify_repo.py fails build if any markdown beyond three intended cavecrew agents appears under agents/.

commands/*.md shadows same-named skills — keep the .md stubs unique

Claude Code loads commands/*.md as flat skills alongside skills/*/SKILL.md, so a stub sharing a skill's name registers that name twice. caveman.md, caveman-commit.md, caveman-review.md and caveman-stats.md each duplicated a real skill — a 3-line stub competing with the full ruleset for the same slash command — and were removed. The .toml stubs are Codex/Gemini-only and are not scanned, so commands/ keeps one .md: caveman-init.md, which has no skill twin. Guarded by tests/verify_repo.py.

Auto-generated / auto-synced — do not edit directly

We removed the agent-specific dotdir mirrors at the repo root (.cursor/, .windsurf/, .clinerules/, .github/copilot-instructions.md, root caveman/SKILL.md). They were never read by the installer — only used to self-apply caveman to this repo when a maintainer opened it in Cursor/Windsurf/Cline. Devs who want caveman in their editor while editing this repo should run npx caveman --with-init once (writes per-repo rule files from src/rules/caveman-activate.md via src/tools/caveman-init.js). For per-user installs through the upstream skills CLI, npx caveman --only <agent> runs npx skills add ... -a <profile>.

A handful of dotdir leftovers (.junie/, .kiro/, .roo/, .agents/) still hold a stale cavecrew/SKILL.md mirror from before the cleanup. They aren't read by anything in the current install path; remove on sight, no migration needed.

What's left is the Claude Code plugin distribution (required by the plugin loader) and the release ZIP.

FileSynced from
plugins/caveman/skills/caveman/SKILL.mdskills/caveman/SKILL.md
plugins/caveman/skills/caveman-compress/SKILL.md (+ scripts/)skills/caveman-compress/SKILL.md (+ scripts/)
plugins/caveman/skills/cavecrew/SKILL.mdskills/cavecrew/SKILL.md
plugins/caveman/agents/cavecrew-*.mdagents/cavecrew-*.md
dist/caveman.skillZIP of skills/caveman/ directory (gitignored; rebuilt by CI on release)

Skills not in this table (caveman-commit, caveman-review, caveman-help, caveman-stats) are not mirrored into the Claude Code plugin distribution by CI. They reach Claude Code through the standalone hook + skill install path, and reach other agents via npx skills add. A plugins/caveman/skills/caveman-stats/ directory is currently checked in as a hand-committed copy; the sync workflow does not touch it, so don't rely on edits there to propagate.


CI sync workflow

.github/workflows/sync-skill.yml triggers on main push when skills/**/SKILL.md or agents/cavecrew-*.md changes.

What it does:

  1. Copies skills/caveman/SKILL.md and skills/cavecrew/SKILL.md into their plugins/caveman/skills/<name>/ mirrors so the Claude Code plugin loader sees the latest behavior.
  2. Copies skills/caveman-compress/SKILL.md and its scripts/ into plugins/caveman/skills/caveman-compress/.
  3. Copies agents/cavecrew-*.md into plugins/caveman/agents/.
  4. Rebuilds dist/caveman.skill (ZIP of skills/caveman/) for the release artifact.
  5. Commits and pushes with [skip ci] to avoid loops.

CI bot commits as github-actions[bot]. After PR merge, wait for workflow before declaring release complete.

The old steps that mirrored SKILL.md and rules into root dotdirs (.cursor/, .windsurf/, .clinerules/, .github/copilot-instructions.md) are gone — those mirrors no longer exist. The old caveman-compress/ → skills/compress/ rename-on-sync is also gone now that compress lives at skills/caveman-compress/.


Hook system (Claude Code)

Four hooks in src/hooks/ plus a caveman-config.js shared module, a caveman-parse.js shared mode-change parser and a package.json CommonJS marker.

Mode state is per session. Each session's mode lives in $CLAUDE_CONFIG_DIR/.caveman-sessions/<session_id>.mode, keyed by the session_id Claude Code puts in every hook payload and in the statusline's stdin JSON. $CLAUDE_CONFIG_DIR/.caveman-active survives as a last-write-wins compat mirror (falls back to ~/.claude/).

SessionStart hook ──┐                                        ┌── UserPromptSubmit hook
  (session_id,      │                                        │     (session_id, prompt)
   source)          ▼                                        ▼
        $CLAUDE_CONFIG_DIR/.caveman-sessions/<session_id>.mode
                             │             │
                          mirrors       reads
                             ▼             ▼
              .caveman-active      caveman-statusline.sh ◀── session JSON on stdin
           (last-write-wins,        [CAVEMAN] / [ULTRACAVE] / [MEGACAVE]
            compat only)

Two invariants that are load-bearing, not stylistic:

  • The legacy mirror never holds the literal off. Deactivation deletes it. off is in VALID_MODES, so an older caveman-mode-tracker.js reading off from that path would pass its !INDEPENDENT_MODES.has(...) check and inject "CAVEMAN MODE ACTIVE (off)", and an older caveman-statusline.sh would render [CAVEMAN:OFF]. Mixed-version installs are real: plugin hooks and standalone hooks can both be registered at once, and statusLine holds an absolute path baked in at install time.
  • recordModeChange compares against the session's own state, never the mirror. That's why readSessionModeRaw exists alongside resolveActiveMode — diffing against the mirror would compare one session's transition to whatever another session wrote last and spam the log with phantom entries.

Both spellings of off are READ everywhere: a missing file (old semantics) and a literal off (new, durable).

src/hooks/package.json pins the directory to {"type": "commonjs"} so the .js hooks resolve as CJS even when an ancestor package.json (e.g. ~/.claude/package.json from another plugin) declares "type": "module". Without this, require() blows up with ReferenceError: require is not defined in ES module scope.

All hooks honor CLAUDE_CONFIG_DIR for non-default Claude Code config locations.

src/hooks/caveman-config.js — shared module

Exports:

  • getDefaultMode() — resolves default mode in order: CAVEMAN_DEFAULT_MODE env var → repo-local config (<cwd>/.caveman/config.json or <cwd>/.caveman.json, walking up to the filesystem root) → user config ($XDG_CONFIG_HOME/caveman/config.json / ~/.config/caveman/config.json / %APPDATA%\caveman\config.json) → 'caveman'. The env var short-circuits before any cwd walk. Repo-local config lets a team check in a per-project default without polluting every contributor's env or user config.
  • findRepoConfigPath(start) — walks up from start (default process.cwd()) looking for the first .caveman/config.json or .caveman.json. Bounded to 64 ancestors. Refuses symlinked files (symmetric with safeWriteFlag / readFlag).
  • safeWriteFlag(flagPath, content) — symlink-safe flag write. Refuses if flag target or its immediate parent is a symlink. Opens with O_NOFOLLOW where supported. Atomic temp + rename. Creates with 0600. Protects against local attackers replacing the predictable flag path with a symlink to clobber files writable by the user. Used by both write hooks. Silent-fails on all filesystem errors.
  • validateSessionId(id) — returns the id or null. Whitelist ^[A-Za-z0-9_-]{1,128}$. A session id becomes part of a filesystem path, so nothing may interpolate one without passing it through here first. The symlink hardening in safeWriteFlag does not cover traversal.
  • resolveActiveMode(claudeDir, sessionId) — what mode is in effect: session file → legacy mirror, with both a missing file and a literal off collapsing to null. The reader every hook should use.
  • readSessionModeRaw(claudeDir, sessionId) — literal stored value for this session, no legacy fallback. Two callers only: recordModeChange (see the invariant above) and the SessionStart continuation branch, which has to tell a stored off apart from "nothing stored yet".
  • writeSessionMode(claudeDir, sessionId, modeOrNull) — the single writer. Writes off literally to the session file, unlinks the legacy mirror. Rejects anything outside VALID_MODES.
  • writeSessionPrev / readSessionPrev / clearSessionPrev — the displaced-prose-mode memory for one-shot skills (#599), scoped per session so two windows running /caveman-commit don't overwrite each other's return target.
  • gcSessionStore(claudeDir, opts) — mtime sweep of .caveman-sessions/, 14-day TTL (CAVEMAN_SESSION_TTL_MS overrides for tests), maxDeletes cap, refuses symlinks. Called from SessionStart on new sessions only — never on compact, which is frequent and shares the 5s hook budget.
  • recordModeChange(claudeDir, newMode, sessionId) — third arg tags the log entry; omitted (not null) when unknown, so pre-existing readers see the old shape.

Every function above accepts a sessionId that may be null or malformed and degrades to the legacy machine-wide behavior. That is the entire backward-compatibility story: the old code path is the fallback branch, which is why the existing tests — none of which send a session_id — pass unchanged. The three hook entrypoints resolve these helpers individually (cfg.writeSessionMode || <legacy stub>) rather than adding them to their requireSibling shape checks: a caveman-config.js from before per-session state satisfies those checks, and hard-failing over the newer exports would trade "machine-wide mode, as it always worked" for "no state at all" on exactly the plugin-cache-drift scenario #848 is about.

src/hooks/caveman-activate.js — SessionStart hook

Runs on every SessionStart — source is startup, resume, clear, compact or fork. Four things:

  1. Reads the hook payload from stdin for session_id, source and cwd
  2. Resolves this session's mode and persists it via writeSessionMode
  3. Emits caveman ruleset as hidden stdout — Claude Code injects SessionStart hook stdout as system context, invisible to user
  4. Checks settings.json for statusline config; if missing, appends nudge to offer setup on first interaction (one-shot, gated by .caveman-nudge-shown)

source branching. RESET_SOURCES is {startup, clear} — only those re-derive getDefaultMode(). Everything else (compact/resume/fork, an unrecognized source, and the watchdog's unknown) READS the stored mode via readSessionModeRaw, falling back to the legacy mirror for a session that predates the store, and only then to the default. Reading the LITERAL value is the point: #691 already branched on source, but it read the legacy flag where off is spelled "no file", so a deactivated session found nothing and re-derived the default anyway — "stop caveman" was still undone by the next auto-compaction. A stored off now short-circuits to OK with no ruleset. It still re-emits when a mode IS active: compaction is what prunes the rules out of context, which is the whole reason this hook runs on every source. clear counts as a fresh start (explicit user reset — nothing else in the conversation survives it, so neither should a "stop caveman" from before it); this deliberately differs from #691's comment, which grouped clear with the continuations.

Headless sessions start under manual (#377). Whenever the hook falls back to the configured default (startMode()), a CLAUDE_CODE_ENTRYPOINT starting with sdk- (claude -p, Agent SDK) turns any non-off default into manual: tool probes that parse the reply get no ruleset until an explicit /caveman. Interactive entrypoints (cli, claude-vscode, claude-desktop, …) never match; an explicit CAVEMAN_DEFAULT_MODE env var opts back in. Hook test helpers drop CLAUDE_CODE_ENTRYPOINT from the env, or a headless runner flips every SessionStart test.

The stdin contour is load-bearing. Activation runs on the first COMPLETE JSON object, not at EOF, with a 2000ms PAYLOAD_WATCHDOG_MS backstop and process.stdin.unref() in finish(). pause() alone is not enough — it stops reading but leaves the pipe handle referenced, so a host that holds the write end open (Windows pipe close lags arbitrarily, #729/#833) burns the whole 5s budget. The watchdog must NOT assume startup: it reports unknown, which preserves the session's stored mode rather than resetting it. Several suites also invoke this hook via subprocess.run() with no input=, inheriting a stdin that never reaches EOF — tests/test_hooks.py::test_hook_never_blocks_on_stdin_that_never_closes guards that path.

Silent-fails on all filesystem errors — never blocks session start.

--subagent: the SubagentStart hook (#621). SessionStart context reaches only the parent thread, so the same script is registered for SubagentStart with --subagent (plugin.json, installer/install.js, src/hooks/install.{sh,ps1}). It reuses the stdin reader and watchdog, resolves THIS session's mode via resolveActiveMode (a session with no stored state at all — SessionStart skipped or failed, or the session predates the store — falls back to the last-write-wins legacy mirror, the same degrade the tracker's reinforcement uses, so a subagent matches its parent thread; pinned in tests/hooks/subagent-start.test.mjs), and prints {"hookSpecificOutput":{"hookEventName":"SubagentStart","additionalContext":<that mode's skill>}}. Nothing is printed when the session stored off (no "stop caveman" leak, #672; another window's mode never overrides a stored off), in a one-shot mode, for cavecrew-* agents (they carry their own ultracave voice), or under a repo defaultMode: "off" (#634, same gate as the tracker's reinforcement). Read-only: no mode write, no mode log, no GC, no nudge, no cavecrew model overrides. Uninstall needs nothing extra — removeCavemanHooks walks every event and matches the caveman-activate.js basename.

src/hooks/caveman-mode-tracker.js — UserPromptSubmit hook

Reads JSON from stdin — session_id scopes every read and write. Three responsibilities:

1. Slash-command activation. If prompt starts with /caveman, writes the session's mode via writeSessionMode:

  • /caveman → configured default (see caveman-config.js, defaults to caveman)
  • /ultracave (alias /caveman ultra) → ultracave
  • /megacave (alias /caveman wenyan, any wenyan-*) → megacave
  • /caveman lite or /caveman full (legacy aliases) → caveman
  • /caveman-commit → commit
  • /caveman-review → review
  • /caveman-compress → compress

2. Natural-language activation/deactivation. Matches phrases like "activate caveman", "turn on caveman mode", "talk like caveman" and writes the configured default mode. Matches "stop caveman", "disable caveman", "normal mode", "deactivate caveman" etc. and stores a durable off for the session. README promises these triggers, the hook enforces them.

3. Per-turn reinforcement. When the session's mode is a non-independent one (i.e. not commit/review/compress), emits a small hookSpecificOutput JSON reminder so the model keeps caveman style after other plugins inject competing instructions mid-conversation. The full ruleset still comes from SessionStart — this is just an attention anchor.

src/hooks/caveman-statusline.sh — Statusline badge

Reads the session JSON Claude Code pipes to it on stdin, extracts session_id (pure bash — no jq dependency), and reads .caveman-sessions/<id>.mode; falls back to $CLAUDE_CONFIG_DIR/.caveman-active when there is no usable id. Outputs colored badge string for Claude Code statusline:

  • caveman or empty → [CAVEMAN] (orange)
  • off → nothing at all. Never [CAVEMAN:OFF] — that reads as a mode rather than the absence of one
  • ultracave → [ULTRACAVE], megacave → [MEGACAVE]; one-shots → [CAVEMAN:<MODE_UPPERCASED>] (orange). Legacy stored values render through the same map

Stdin is bounded the same way as the SessionStart hook: [ ! -t 0 ] skips an interactive terminal, and read -r -d '' -t 1 caps the wait. The timeout is an integer on purpose — macOS ships bash 3.2, which rejects -t 0.3 with invalid timeout specification.

The statusline reports mode only. Old .caveman-statusline-suffix values are ignored; fixed-ratio savings estimates were removed because no measured baseline supported them. /caveman-stats records actual Claude output/cache-read counts and mode attribution with savings unknown. Gemini uses native /stats model and /stats session; never read Claude transcripts as another host's usage.

Configured in settings.json under statusLine.command. PowerShell counterpart at src/hooks/caveman-statusline.ps1 for Windows. Both scripts symlink-refuse and whitelist-validate mode flag contents — never echo arbitrary bytes.

src/hooks/caveman-stats.js --record — SessionEnd hook

Same script as /caveman-stats; the --record flag turns it into a silent recorder. Reads the SessionEnd payload (session_id, transcript_path) and appends one snapshot of that transcript's totals to $CLAUDE_CONFIG_DIR/.caveman-history.jsonl, so lifetime history fills in without the user running /caveman-stats. Only the transcript the host names is read — never the newest file on disk. Stdin follows the activate-hook contour: first complete JSON object, 2000ms PAYLOAD_WATCHDOG_MS (gives up without recording), unref() on finish. Always exits 0 and writes nothing to stderr — a SessionEnd failure would surface as a hook error while the user quits. Every installer (plugin manifest, installer/install.js, src/hooks/install.{sh,ps1}) writes --record unquoted after the quoted script path; rewriteLegacyManagedHookCommands carries trailing --flag args over when it moves a bare-node command to the absolute node path.

src/hooks/caveman-host-session-start.js — Cursor / Copilot CLI sessionStart hook

node caveman-host-session-start.js <host>, one script for hosts other than Claude Code. Emits the resolved mode's ruleset under the host's key: additional_context for cursor, additionalContext for copilot. The mode is getDefaultMode() resolved from the session's workspace (cwd, Cursor's workspace_roots or CURSOR_PROJECT_DIR), so defaultMode and repo .caveman.json apply; off, manual, one-shot modes, an unknown host or any failure print {}. Writes no state. The owned install at <host>/caveman/hooks/ reads its pinned <host>/caveman/skills/ before the shared resolver, which would otherwise find the host's unpinned npx skills copy first. Stdin follows the activate-hook contour.

Hook installation

Plugin install — hooks wired automatically by plugin system.

Standalone install — installer/install.js (the unified Node installer) copies hook files into $CLAUDE_CONFIG_DIR/hooks/ and merges SessionStart + SubagentStart + UserPromptSubmit + SessionEnd + statusline into settings.json. Uses the JSONC-tolerant helpers in installer/lib/settings.js so a commented settings.json no longer crashes the merge. Defensive validateHookFields runs before every write to prevent a single malformed hook from poisoning the entire file (Claude Code Zod silently discards the whole settings.json on schema mismatch).

The install.sh / install.ps1 shims at the repo root delegate to installer/install.js via node (local clone) or npx -y github:JuliusBrussee/caveman (curl|bash). No legacy fallback path remains — earlier install.sh.legacy / install.ps1.legacy files were removed.

Uninstall — npx -y github:JuliusBrussee/caveman -- --uninstall (or node installer/install.js --uninstall from a clone). Strips caveman hook entries from settings.json via substring marker caveman, deletes hook files, and removes the Claude plugin / Gemini extension. Native skill copies use installer/lib/provider-skills.js and ownership journals; uninstall removes unchanged owned files while preserving foreign or edited content. Use the same vendor home overrides for install and uninstall. Delegated npx skills add installs remain managed by the host/upstream skill manager.


Skill system

Skills = Markdown files with YAML frontmatter consumed by Claude Code's skill/plugin system and by npx skills for other agents.

Each skill has a human-facing README.md alongside the LLM-facing SKILL.md. The README explains what the skill does for users browsing GitHub; the SKILL.md is the prompt body the agent loads. Don't merge them — different audiences, different formats.

Three skills, one mode model

Three self-contained skills, each 1:1 with a stored mode id: caveman (default voice), ultracave (grammar stripped), megacave (文言文). The SessionStart hook injects skills/<id>/SKILL.md whole; there is no per-level filtering. Legacy stored values map on read: lite and full → caveman, ultra → ultracave, every wenyan* → megacave. Writes emit new ids only. Mode persists until changed or session ends. Plan: docs/technical/three-skills-refactor.md.

defaultMode: "manual" is a Claude Code startup policy, not a fourth mode. It starts inactive, stores only off, and explicit bare-command or natural-language activation resolves to caveman. Keep it out of VALID_MODES; only configuration accepts it. OpenCode maps this policy to its existing caveman default because its installer also supplies static AGENTS.md activation. /caveman status is read-only and must return before one-shot restoration or any mode-state mutation.

Auto-clarity rule

Caveman drops to normal prose for: security warnings, irreversible action confirmations, multi-step sequences where fragment ambiguity risks misread, user confused or repeating question, questions the agent asks the user. Resumes after. Defined in skill — preserve in any SKILL.md edit.

caveman-compress

Sub-skill in skills/caveman-compress/SKILL.md. Takes file path, compresses prose to caveman style, writes to original path, saves backup at <filename>.original.md. Validates headings, code blocks, URLs, file paths, commands preserved. Retries up to 2 times on failure with targeted patches only. Requires Python 3.10+.

The slash command is /caveman-compress everywhere — same name in plugin and standalone install. CI no longer renames the directory on sync (the old caveman-compress/ → skills/compress/ sed rename is gone now that the source lives at skills/caveman-compress/).

caveman-commit / caveman-review

Independent skills in skills/caveman-commit/SKILL.md and skills/caveman-review/SKILL.md. Both have own description and name frontmatter so they load independently. caveman-commit: Conventional Commits, ≤50 char subject. caveman-review: one-line comments in L<line>: <severity> <problem>. <fix>. format.


Agent distribution

How caveman reaches each agent type:

AgentMechanismAuto-activates?
Claude CodePlugin (hooks + skills) or standalone hooksYes — SessionStart hook injects rules; SubagentStart passes the session's mode to subagents
Codexnpx skills add -a codex plus an owned SessionStart hook in $CODEX_HOME/hooks.json (installer, default on); plugin in plugins/caveman/; repo .codex/hooks.json and .codex/config.toml for this checkoutYes — SessionStart hook, after the user trusts it once via /hooks
Gemini CLIExtension with GEMINI.md context fileYes — context file loads every session
opencodeNative plugin (src/plugins/opencode/) copied into ~/.config/opencode/plugins/caveman/ + AGENTS.md ruleset + skills/agents/commands directories. Plugin uses session.created and tui.prompt.append lifecycle hooks. No statusline (opencode TUI exposes no plugin-writable badge).Yes — session.created writes flag, AGENTS.md carries always-on ruleset
OpenClawWorkspace skill at ~/.openclaw/workspace/skills/caveman/SKILL.md (frontmatter merged with version + always: true) plus a marker-fenced bootstrap block in ~/.openclaw/workspace/SOUL.md. Both writes go through installer/lib/openclaw.js; workspace path is overridable via OPENCLAW_WORKSPACE.Yes — SOUL.md is auto-injected each turn under "Project Context" (subject to OpenClaw's 12K-per-file / 60K-total bootstrap caps)
CursorPlugin (.cursor-plugin/plugin.json: skills + hooks/hooks-cursor.json sessionStart hook) or --only cursor: npx skills add ... -a cursor -g skills, then installer/lib/cursor-native.js owns cavecrew agents in ~/.cursor/agents/ (Claude model: dropped, readonly: true on investigator/reviewer) and the shared hook payload in ~/.cursor/caveman/ with one sessionStart entry merged into ~/.cursor/hooks.json. Both hooks run src/hooks/caveman-host-session-start.js cursor. Per-repo .cursor/rules/caveman.mdc via --with-init. Pick plugin OR --only cursor, not both (two hooks inject twice).Yes — sessionStart additional_context, honors defaultMode (off/manual inject nothing)
Windsurfnpx skills add ... -a windsurf (default via --only windsurf); per-repo .windsurf/rules/caveman.md via --with-initYes — always-on rule
Clinenpx skills add ... -a cline (default via --only cline); per-repo .clinerules/caveman.md via --with-initYes — Cline auto-discovers .clinerules/
Copilotnpx skills add ... -a github-copilot (detected via the copilot CLI plus its ~/.copilot dir, since AWS Copilot CLI ships a copilot binary too, or the VS Code/Cursor extension); per-repo .github/copilot-instructions.md + AGENTS.md via --with-init. When the Copilot CLI is present (copilot on PATH or $COPILOT_HOME set — not a bare ~/.copilot, which npx skills add -a github-copilot itself creates) the installer also owns $COPILOT_HOME/caveman/ (shared hook payload) and $COPILOT_HOME/hooks/caveman.json (sessionStart → caveman-host-session-start.js copilot); --no-hooks skips it. No userPromptSubmitted hook: Copilot drops its output.Copilot CLI: yes — sessionStart additionalContext, honors defaultMode. VS Code Copilot Chat: repo-wide instructions via --with-init
Grok BuildOwned skill copies into $GROK_HOME/skills (default ~/.grok) plus a marker-fenced ruleset block in $GROK_HOME/AGENTS.md, appended/stripped through installer/lib/openclaw.js's marker helpersYes — Grok loads ~/.grok/AGENTS.md as global rules (docs-backed; no live binary run)
Antigravity CLI (agy)--only antigravity-cli stages a temp plugin (plugin.json, the nine skills, rules/AGENTS.md = src/rules/caveman-activate.md) and runs agy plugin install <dir>; agy copies it to ~/.gemini/config/plugins/caveman/ and owns it. Uninstall: agy plugin uninstall caveman when agy plugin list names it. No session-start hook exists in agy (PreInvocation fires before every model call), so no defaultMode.Yes — plugin rules/AGENTS.md merges into agy's active rule set (verified agy 1.2.17)
Continue, AiderDesk, Antigravity IDE/2.0Owned physical copies into each vendor's supported directory, honoring configured homes; separate explicit targets for the two Antigravity productsHost skill invocation; feature settings may be required
Others (Junie, Trae, Warp, Tabnine, Mistral, Qwen, Devin, Droid, ForgeCode, Bob, Crush, iFlow, OpenHands, Qoder, Rovo Dev, Replit, …)Delegated npx skills personal installs; Replit uses project scope. Configured iFlow/Crush roots use owned copiesHost skill invocation; /caveman where supported

opencode reaches Tier 1 minus the statusline (opencode's TUI has no plugin-writable badge). Mode flag lives at ~/.config/opencode/.caveman-active for any external tooling that wants to surface it.

For agents without hook systems, the always-on snippet lives in INSTALL.md's "Want it always on?" section — keep current with src/rules/caveman-activate.md.

Adding a new agent. Edit the PROVIDERS array in installer/install.js — single source of truth, no more bash/PS1 dual-source drift. Each entry has id, label, mech, detect (clause spec like command:foo||dir:$HOME/x; && joins terms that must all hold), optional profile (vercel-labs/skills slug), optional soft: true (config-dir-only detection).

  1. The profile slug must exist in upstream vercel-labs/skills. Verify against the README before merging — wrong slugs cause npx skills add to fail at runtime, not at install-script load.
  2. Run node installer/install.js --list to confirm the new row renders correctly.
  3. Soft probes (config-dir-only) are fine but tag them with soft: true. They render with (soft) in --list so users know detection is best-effort.

Evals

evals/ has three-arm harness:

  • __baseline__ — no system prompt
  • __terse__ — Answer concisely.
  • <skill> — Answer concisely.\n\n{SKILL.md}

Honest delta = skill vs terse, not skill vs baseline. Baseline comparison conflates skill with generic terseness — that cheating. Harness designed to prevent this.

llm_run.py calls claude -p --system-prompt-file ... --output-format json per (prompt, arm), isolated from user settings, plugins and MCP, saves text plus Claude-reported usage (input/output/cache tokens, cost) to evals/snapshots/results.json (results.<lang>.json with CAVEMAN_EVAL_LANG). measure.py reads snapshot offline with tiktoken (OpenAI BPE — approximates Claude tokenizer, ratios meaningful, absolute numbers approximate). CAVEMAN_EVAL_SET=fidelity runs the regex-checked cases in evals/prompts/fidelity.json into snapshots/fidelity.json; score_fidelity.py scores it offline.

Add skill: drop skills/<name>/SKILL.md. Harness auto-discovers. Add prompt: append line to evals/prompts/en.txt.

Snapshots committed to git. CI reads without API calls. Only regenerate when SKILL.md or prompts change.


Benchmarks

benchmarks/ runs real prompts through Claude API (not Claude Code CLI), records raw token counts. Results committed as JSON in benchmarks/results/. Benchmark table in README generated from results — update when regenerating.

To reproduce: uv run python benchmarks/run.py (needs ANTHROPIC_API_KEY in .env.local).


Key rules for agents working here

  • Edit skills/<name>/SKILL.md for behavior changes. Never edit synced copies under plugins/caveman/skills/.
  • Edit src/rules/caveman-activate.md for auto-activation rule changes. Never edit any per-agent rule copy a user has on their machine.
  • Edit src/rules/caveman-openclaw-bootstrap.md for the OpenClaw SOUL.md bootstrap snippet. Keep the <!-- caveman-begin --> / <!-- caveman-end --> markers and the Respond terse like smart caveman sentinel — installer/lib/openclaw.js keys idempotency off both. If you change the embedded fallback in installer/lib/openclaw.js, keep it byte-equivalent to the file.
  • Per-skill human docs live in skills/<name>/README.md. The LLM-facing body is in SKILL.md. Don't merge them — different audiences.
  • Build artifacts go in dist/. Never check files into dist/ manually — CI rebuilds them on push, and dist/ is gitignored.
  • README most important file for user-facing impact. Optimize for non-technical readers. Preserve caveman voice.
  • INSTALL.md is the per-agent install reference. Keep the install table in README.md short and link out to INSTALL.md for the full matrix.
  • Benchmark and eval numbers must be real. Never fabricate or estimate.
  • CI workflow commits back to main after merge. Account for when checking branch state.
  • Hook files must silent-fail on all filesystem errors. Never let hook crash block session start.
  • Any new flag file write must go through safeWriteFlag() in caveman-config.js. Direct fs.writeFileSync on predictable user-owned paths reopens the symlink-clobber attack surface.
  • Mode state reads/writes go through the caveman-config.js session helpers (resolveActiveMode / writeSessionMode / …), never by joining a path by hand. Any session_id that reaches a path must pass validateSessionId() first.
  • Keep mode-state logic in caveman-config.js. src/plugins/opencode/plugin.js cannot require() from disk (compiled Bun binary) — it reads that file and evaluates it through new Function(...) with a createRequire that resolves only node built-ins. Parsing already lives in caveman-parse.js (#602) and loads the same way, so a second parser copy is no longer the risk; a THIRD home for the state primitives would be, because opencode would keep its own drifting version of them.
  • The two statusline scripts have no shared runtime with the JS, so they re-implement path resolution by hand. tests/verify_repo.py::verify_powershell_static greps both against the SESSIONS_DIRNAME and SESSION_ID_RE constants to catch drift — there is no behavioral .ps1 test on the POSIX runners, so that grep is the only guard on the Windows badge.
  • Editing anything in src/hooks/ means regenerating src/hooks/checksums.sha256 (same file set, recomputed digests) — tests/verify_repo.py fails the build otherwise, and installer/install.js verifies remote hook downloads against the manifest for the pinned ref.
  • Hooks must respect CLAUDE_CONFIG_DIR env var, not hardcode ~/.claude. Same for installer/install.js / statusline scripts.
  • Any entrypoint that reads a host hook payload from stdin returns on the first complete JSON object, never at EOF. Under the Windows pipe implementation the host's close lags arbitrarily (#729/#833, #949), so a reader that waits for EOF burns the host's whole budget with its work done. The readers that follow this rule: caveman-activate.js, caveman-mode-tracker.js, caveman-stats.js --record and caveman-host-session-start.js (per-chunk parse), packages/cli/src/native-hook-fast.ts (stdin()), readHookStdin() in packages/cli/src/index.ts (shrink-hook, memory recall, native-hook), and readNativeHookPayload in proxy/cmd/caveman-proxy/main.go. A new hook callback uses one of those, not readStdin() or io.ReadAll. Each has a keep-the-writer-open regression test; add one for any new reader.
  • installer/install.js is the only installer source. install.sh / install.ps1 at repo root are 30-line shims that delegate to it. Never re-add per-OS install logic to the shims — that's how we got the Windows quoting bug (#249).
  • Any settings.json read in installer or hooks must go through installer/lib/settings.js readSettings() so JSONC comments don't crash the merge. Any settings.json write must run through validateHookFields() first.