A foreign listener on 127.0.0.1:8787 used to get the banner but still
receive the routed launch, which would send the operator's provider
keys to a process caveman does not own (#945). Treat it like a gate
mismatch and fall back to a direct launch.
Also: Claude Code refuses Remote Control behind any non-first-party
ANTHROPIC_BASE_URL, so `caveman claude remote-control` launches direct
and the limitation is documented (#947); caveman-compress reads the
first text block instead of content[0] so a leading tool_use block no
longer crashes after a paid call (closes#932's report); hook checksum
manifest regenerated after #794.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sonnet 5's $10/M output rate is the permanent standard price -- the
2026-09-01 increase to $15/M was cancelled per Anthropic's pricing docs,
not a temporary intro window. Also removes claude-haiku-5, which does
not exist in Anthropic's current lineup (only Haiku 4.5 ships).
Addresses review feedback on PR #900.
Mode was applied per session but stored in one file per machine
($CLAUDE_CONFIG_DIR/.caveman-active). Four bugs followed from that one
fact: parallel windows shared a mode; deactivation was spelled "no flag
file", so SessionStart re-derived the configured default and undid an
explicit "stop caveman" after every auto-compaction; the statusline badge
rendered whichever window wrote last; and "off" could not survive a
SessionStart at all.
payload's `source`, but it read the legacy flag, where off is absence — a
deactivated session found nothing stored and fell back to getDefaultMode()
regardless. The other three are consequences of the storage shape, not of
when the hook re-derives.
State now lives in .caveman-sessions/<session_id>.mode, keyed by the
session_id Claude Code puts in every hook payload and in the statusline's
stdin JSON. The legacy flag stays as a last-write-wins compat mirror and
never receives the literal 'off': an older hook reading that would inject
"CAVEMAN MODE ACTIVE (off)" and an older statusline would render
[CAVEMAN:OFF]. Both spellings of off are read everywhere — a missing file
(old) and a literal 'off' (new, durable).
caveman-config.js gains the primitives: validateSessionId (a whitelist,
because a session id becomes part of a path), resolveActiveMode,
readSessionModeRaw, writeSessionMode, per-session prev for one-shot skills
(#599), gcSessionStore, and a session_id-tagged recordModeChange (#601).
Every one takes a sessionId that may be null or malformed and degrades to
the previous machine-wide behavior — the old code path IS the fallback
branch, which is why the existing suites, nonid,
pass unchanged.
SessionStart re-derives the default only on `startup` and `clear`; compact,
resume, fork, an unrecognized source and the n`
read the stored value. Grouping `clear` with the resets deliberately
differs from #691's comment: per-session storage makes the distinction
cheap, and nothing else in the conversation survives /clear either. The
watchdog must not reset, or a slow payload on a compaction re-arms a
session the user turned off.
The three entrypoints resolve the new helpers individually instead of
demanding them in their requireSibling shape checks (#848): a
caveman-config.js from before this change satisfies those checks, so
hard-failing would trade "machine-wide mode, no
state at all" on exactly the plugin-cache drift that guard exists for.
Both statusline ports take session_id from the stdin JSON with bounded,
TTY-guarded reads (integer timeout — bash 3.2 rejects fractional read -t)
and render nothing for a deactivated session. verify_repo.py greps them
against SESSIONS_DIRNAME and SESSION_ID_RE, tws
badge. caveman-stats filters the transition l
window's switches no longer skew another's es
(bin/install.js, uninstall.sh, uninstall.ps1) removes the session store;
.caveman-history.jsonl is kept on purpose. opide
behaviour — it writes its own flag in its own config dir and never sees a
Claude Code session_id.
Tests: 25 unit cases in tests/hooks/caveman-config.test.mjs,
SessionStartSourceTests, SessionScopedModeTests, 5 statusline cases, and
tests/manual/session-mode-smoke.sh (22 checks, end-to-end against a
throwaway config dir). docs/testing-session-modes.md carries the full plan
including the live-Claude-Code checks. Hook ced.
npm test 197/197 · tests/*.js clean · unittesss
· smoke 22/22
Signed-off-by: Pavel Filippenko <filippenko.ps@phystech.edu>
priceForModel() only matched Claude 3/4 prefixes, so claude-*-5 model ids
fell through to null and the Est. saved (USD) line, Pricing for <model>
footer, and (~) parenthetical were silently omitted on Claude 5
sessions. Add the Claude 5 rate card at the top of
MODEL_OUTPUT_PRICE_PER_M (Fable/Mythos 5 $50, Opus 5 $25, Sonnet 5
$10 intro through 2026-08-31 then $15 per shared/provider-catalog,
Haiku 5 $5) and extend the prefix-match regression test, including the
[1m] context variant Claude Code reports.
Signed-off-by: Som Samantray <som.samantray@gmail.com>
The UserPromptSubmit stdout/stderr 'error' handling fix in this PR changed
caveman-mode-tracker.js's contents without updating the pinned hash in
checksums.sha256, which tests/verify_repo.py checks in CI.
caveman-mode-tracker.js never installed an 'error' listener on
process.stdout/process.stderr. When the harness closes its end of the
pipe after the hook has already begun writing a payload (the per-turn
reinforcement or the /caveman-stats block), the write throws EPIPE as
an uncaught exception and the hook exits non-zero, surfacing as a red
"Failed to write to socket (Broken pipe, errno 32)" status line under
an otherwise normal prompt.
This mirrors #538, which added the equivalent listener on stdin for
the same reason: hooks must always exit 0, and an unlistened stream
'error' throws regardless of which side of the pipe closed.
Fixes#397
Two changes to how the ruleset instructs, with the behavior it asks for
unchanged.
Replace the shouted persistence clause ("ACTIVE EVERY RESPONSE. No revert after
many turns.") with a plain statement of scope: this is the default style for the
session until the user turns it off. Same instruction, stated once, in the
register the rest of the file uses.
Replace the "No self-reference. Never name or announce the style." prohibition
with the positive form — answer directly in this style, skip the mode tags and
the "Caveman:" recap, because they are redundant with the reply itself. A rule
that says what to do is followed more reliably than one that only forbids.
Also drops the em-dashes from the ruleset prose so the file does not model the
punctuation its own output should avoid. The SessionStart hook body carries the
same two rewrites, with its checksum updated.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
resolvePluginRoot returns the repo root when the hook runs from a clone, so with
any CAVECREW_*_MODEL set every SessionStart rewrote tracked agents/*.md and
dirtied the tree. Skip when the plugin root sits inside a git work tree; an
installed plugin under $CLAUDE_CONFIG_DIR still gets overrides.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmbqWDtNexzDfH5V3BMRz
Uninstall aborts instead of removing hook files whenever settings.json could
not be parsed or rewritten — leaving them while the entries remain is #471 on
every session start. The POSIX and PowerShell uninstallers exit 1 on a missing
node for the same reason.
hooks/package.json is the one HOOK_FILES entry that is not ours: install now
leaves another plugin's manifest alone and uninstall deletes only the exact
one we ship. Temp downloads move to a 0700 mkdtemp so a pre-planted symlink
cannot capture the init script we then execute.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmbqWDtNexzDfH5V3BMRz
Marker handling, both directions. `installOpencode` and caveman-init treated
"both markers present" as a fence, so an orphan BEGIN or an END above a
BEGIN ran the slice arithmetic on end === -1: AGENTS.md re-appended itself
from byte 19 and compounded on every re-run, and a per-repo refresh would
have replaced everything between an orphan BEGIN and a later block's END,
user content included. Both now require exactly one matched pair, in order,
and report damage instead of writing.
Uninstall ordering. uninstall.sh/uninstall.ps1 deleted the hook files first
and edited settings.json second, so a failed or refused edit left Claude
Code pointing at scripts that no longer exist — `Cannot find module
…caveman-activate.js` on every session start (#471). Settings first; files
only once nothing references them. Neither script overwrites an existing
.bak any more (it is the user's only pre-caveman copy), and a JSONC
settings.json now aborts the edit with the working command instead of
half-uninstalling.
install.ps1's "already wired" probe matched the bare substring `caveman`,
which any user hook mentioning the word in a path satisfies (#593) — it
silently skipped wiring and left caveman inactive. It now probes for the
exact script we write per event, mirroring install.sh.
The SessionStart watchdog no longer claims `startup` when it fires. That is
the one source that resets the mode, so a payload slow enough to time out on
a compact/resume event dropped a mid-session `ultra` back to the default.
An unknown source preserves a valid existing flag, and the deadline moves to
3s — still well inside the host's 5s budget, but above a cold Windows/AV
start.
caveman-init writes retry a rename that Windows loses to OneDrive/AV
handles (EPERM/EBUSY/EACCES), the shrink MCP refuses an implausible .cmd
shim before reading it, and pruneOrphanedManagedHooks no longer prunes a
hook whose path is absolute on the other platform — existence is only
knowable for our own.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmbqWDtNexzDfH5V3BMRz
Whole-repo review of the surfaces that mutate a user's agent config, plus a
cross-platform sweep focused on Windows.
Hooks (Windows):
- caveman-mode-tracker finished its work in ~50ms then blocked until the host
killed it at the 5s budget: the 'data' listener REFERENCES the stdin handle,
so pause() alone keeps the event loop alive until the host closes the write
end, and on Windows that close lags arbitrarily. Reproduced off-Windows.
This is the shape of #819. Fixed with process.stdin.unref().
- caveman-activate used readFileSync(0), which blocks INSIDE the read syscall
until EOF — no deadline can interrupt it. The #729/#833 fix had been applied
to the tracker and never to its sibling. Rewritten event-driven, acting on
the first complete payload, with a watchdog so a payload that never arrives
still activates instead of forfeiting the session.
- hookCommand emitted PowerShell call-operator syntax into settings.json, but
Claude Code's Windows hook shell is Git Bash, which rejects it outright:
`syntax error near unexpected token '&'`. Every standalone Windows install
had two dead hooks (#835). Both emitters now route through one bash-safe
shape.
- activate now threads the payload cwd, so a checked-in .caveman.json
defaultMode "off" is honored for the session's directory (#634), and a JSONC
settings.json no longer suppresses the statusline nudge.
Config ownership:
- src/hooks/uninstall.{sh,ps1} matched hooks by the substring "caveman" and
filtered whole entries, deleting user hooks that merely mention the word and
dropping foreign handlers that shared a matcher group (#593). Ported the
exact-basename matcher from bin/lib/settings.js and switched to filtering the
inner handler list. Verified against real PowerShell: the old code left zero
user hooks standing.
- install.{sh,ps1} backed up settings.json unguarded, so a --force reinstall
overwrote the only pre-caveman copy with the merged file. Back up once.
- OpenClaw overwrote a hand-edited workspace skill with no backup.
- caveman-init appended unfenced blocks to tracked repo files, so they could
be neither refreshed nor removed; now marker-fenced and written atomically.
- opencode AGENTS.md and OpenClaw SOUL.md were presence-checked, so a ruleset
edit never reached anyone already installed. Both refresh in place.
- opencode's system-prompt hook had no idempotency guard; it now rewrites its
line rather than stacking another onto the context window.
- readSettings reports when it stripped comments so both write paths warn and
point at the backup instead of silently dropping a user's annotations.
Skills: skills/ is auto-discovered wholesale by the plugin loader, so 13
undocumented skills would have shipped into every user's agent on the next
release. Documented them and added a verify_repo check that fails when a
skills/ directory is named in neither README.md nor CLAUDE.md.
CI: a windows-powershell job covering the surface engine-ci never exercised —
the PowerShell installer round trip, and a bash -n gate over every hook command
the installer writes. That gate alone catches #835.
Tests: every fix above is pinned by a test that fails against the old code,
including tests/test_hook_stdin_lifecycle.js, which reproduces the Windows
lagging-close condition on any platform.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ez7w7BBtKrMZCiZz9ea4cb
Findings from a whole-repo cross-platform audit.
Data loss:
- evals/llm_run.py wrote the snapshot with write_text(ensure_ascii=False) and
no encoding: on Windows that truncates results.json to 0 bytes, then raises
at the first non-cp1252 char. Destroyed the committed snapshot and the run.
- compress.py read the user's target with errors="ignore", silently dropping
undecodable bytes, backing up the mangled text, passing its own readback
check, then overwriting the original. Now decodes strictly and refuses.
Broken on Windows:
- benchmarks/ and evals/ read SKILL.md, README.md and results.json without an
encoding; all three raise on a cp1252 decode.
- cavemem's Python client used text=True with no encoding, so a non-ASCII
memory failed to store and any recall came back mojibake.
- native-hook-fast spawned a second full Node process from inside a hook with
no timeout at all.
Also fixed:
- Backported the pnpm cross-drive shim fix to the five copies that never got
it; hoisted two inline parsers so all of them are unit-testable.
- portable-command now resolves through PATH/PATHEXT instead of silently
no-op'ing on a bare command name.
- uninstall.ps1 writes a temp .js instead of node -e (matches install.ps1).
- Real backoff on the flag rename retry; EPERM/EBUSY retry when replacing a
running binary.
- Hooks act on the first complete JSON payload instead of blocking on EOF;
the stats child timeout drops below the host's hook budget.
- Statusline tests run the .ps1 on Windows instead of skipping.
- Added the macOS CI job the repo never had.
Left unfixed and annotated in place: the win32 hookCommand emits PowerShell
call-operator syntax while #835 describes the runner as git-bash /bin/sh.
Needs a real Windows host to settle; guessing would break whichever works.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NveRZs9mV97iX7HSgq3BYF
- Installer now prefers opencode.jsonc over opencode.json, preventing
shadow config that clobbers existing plugins (#861)
- Bump install.sh PINNED_REF from v1.10.0 to v2.1.0 (#861)
- Add explicit agents array to plugin.json so CLAUDE.md and AGENTS.md
in agents/ are not auto-discovered as agent definitions (#855)
- caveman-shrink now always walks nested inputSchema descriptions
instead of skipping them when top-level descriptions exist (#851)
Co-authored-by: Cursor <cursoragent@cursor.com>
Two mode-parser defects, both silent.
1. Punctuation glued to the level. "/caveman ultra; still too verbose" parsed
the argument as "ultra;", matched no mode, left the level untouched and said
nothing. Only parts[1] is ever read, so trailing WORDS were already
harmless — it was the glued character. Leading punctuation is stripped too,
so /caveman "ultra" resolves rather than failing the same silent way.
An argument that still resolves to nothing now returns
{ action: 'unresolved' } instead of null, and the tracker turns that into a
notice naming the valid levels. The list is derived from VALID_MODES minus
'off', the independent modes, and 'wenyan' — the storage alias for
wenyan-full, which would otherwise advertise seven levels for a product
documented as having six. The rejected argument is never echoed: it is
untrusted input headed for model context. An independent mode is pointed at
its own command instead of being reported as unrecognized, because it is a
real mode the user can see in the docs.
'unresolved' is additive — applyModeChange in the opencode plugin acts only
on set/clear. The notice does not early-return, so a typo cannot strand the
user in a one-shot /caveman-commit for an extra turn (#599) or cost that
turn its reinforcement.
2. Prose that quotes the triggers fires them. Natural-language matching ran
over the whole prompt, so a bug report quoting the /caveman-help card's own
line — Say "stop caveman" or "normal mode". — switched caveman off mid-task.
#537's envelope unwrap closed the slash path; this is the prose path.
Quoted spans are blanked before matching. A prompt-length cap was tried
first and is worse on both sides: it still fires on any short quoted line,
and it drops the compound instructions people actually type ("turn off
caveman and write the release notes for ..."). That direction is the
dangerous one — a false activation is a nuisance, but a dropped
deactivation leaves the user unable to escape the mode with no feedback.
Only " and ` delimit; apostrophes are too common in English to pair on.
A slash-initiated prompt no longer fires natural-language triggers either,
symmetric with the skipNaturalLanguage a foreign command envelope already sets.
caveman-activate.js, caveman-mode-tracker.js and caveman-stats.js required
./caveman-config at module top level with no guard, so an install missing that
one file produced an uncaught MODULE_NOT_FOUND — a raw Node stack trace and
exit 1 on every session start and every prompt, surfaced only as:
SessionStart:startup hook error
Failed with non-blocking status code: node:internal/modules/cjs/loader:1408
caveman-parse.js already wrapped the identical import; the risk was understood,
just not applied to the mandatory dependency.
Each entrypoint now resolves its siblings through a small requireSibling() and
degrades: activate emits its ruleset and exits 0 (only flag persistence is
lost), the tracker emits nothing and exits 0 with stdin drained, and stats
prints one actionable line instead of a stack trace. The loader is duplicated
per entrypoint on purpose — a shared one would be another sibling that can go
missing, which is the failure being guarded against.
Three things the guard has to get right:
- Shape, not just throw. A module that LOADS but exports a stale shape is the
plugin-cache-drift case the issue actually describes; without a shape check
the destructure succeeds and the first use dereferences undefined, producing
exactly the crash this removes.
- Missing vs. broken. A MODULE_NOT_FOUND thrown from inside a sibling that
loaded fine is reported as a load failure, not an incomplete install, and the
.cjs retry is gated on the error naming the module we asked for so it cannot
blame a file that was never meant to exist.
- Degrading must not invert intent. The fallback getDefaultMode mirrors the
real resolution order — env var, repo-local .caveman.json, user config —
rather than reading only the env var. Ignoring a checked-in defaultMode
"off" would force caveman ON for a team that opted out the moment one file
went missing.
Fixes#848
Close hook stdin, use explicit UTF-8 for Python test files, and skip the POSIX shim test on Windows. Keep the hook checksum manifest in sync.\n\nRefs #823
Remove broken internal links, add shrink package coverage, pin CI actions, skip Go-backed JavaScript tests when Go is absent, and surface hook cleanup failures in debug mode.\n\nRefs #825
Single-commit port of the Caveman 2 tree onto the original repo history
(candidate caveman-2@88878a4 applied over main@3098342). No unrelated-history
merge; original history untouched.
Preserved v1 surfaces:
- skills/caveman/SKILL.md byte-for-byte, plus plugin mirror and caveman.skill zip
- legacy CI (Node 18/20/22 installer + Python 3.11 suites) alongside engine-ci
- cli/install.js compatibility shim forwarding to bin/install.js
- .codex/config.toml ships both hooks and codex_hooks for older Codex (#617)
- README Trendshift + Skills.sh badges and local star-history asset
- v1.10.0-pinned curl installs; benchmarks/tmp-starcharts ignore rules
Fixed for public same-repo release:
- SECURITY.md vulnerability reporting points at this repository
- release-binaries.yml publishes to this repo via job-scoped GITHUB_TOKEN
(cross-repo PAT removed); binary-release environment still gates signing
- packages/cli/PUBLISHING.md rewritten for packages/ layout and same-repo flow
- profile-contract and provider-catalog lanes skip lane-scope gates on
initial import so the launch PR can gate on their real validation steps
- ANNOUNCEMENT.md network claim corrected: compression is local; requests
still go to the provider you configured
Caveman 2 stays opt-in: bin-v1.0.0 binaries and @caveman-ai/cli publish
separately after this lands and soaks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BLvb5QniwYjmcawyssQNw9
Marketplace fix (#712, #705): Claude Desktop rejects plugins containing a
top-level bin/ directory, and .claude-plugin/marketplace.json packages the
repo root, so the installer directory is now cli/. Every reference updated
(package.json bin entry + files, shims, docs, tests, caveman-init require
path). Supersedes PR #726.
Security (PR #717 verified): quoteWinArg only quoted on whitespace/quotes,
leaving cmd.exe metacharacters (& | ^ < > % parens) unescaped on the
shell:true Windows spawn path. Attacker-influenced arguments (--with-init
cwd, --with-mcp-shrink value) could chain commands. Trigger regex now
covers the metacharacter set; quoting logic split into a platform-
independent, unit-tested helper.
Also:
- uninstall removes .caveman-active.prev, .caveman-mode-log.jsonl,
.caveman-statusline-suffix, .caveman-nudge-shown; keeps
.caveman-history.jsonl with a printed note; dry-run now says
'would remove' instead of lying (#635, supersedes PRs #693#636)
- Array.isArray guard in rewriteLegacyManagedHookCommands — malformed
hook event no longer crashes the installer mid-run (supersedes PR #646)
- gemini extensions install --consent: the security prompt hung every
piped/non-interactive install forever (#676, part of PR #664)
- OpenClaw skill stamps the real PINNED_REF version instead of hardcoded
1.0.0; new --no-always flag for load-on-demand installs (supersedes
PR #720)
- shims scope NPM_CONFIG_ALLOW_GIT=all to the npx call — npm >=12
defaults allow-git to none and EALLOWGITs github: installs (#698)
- .codex/config.toml ships hooks + codex_hooks keys so auto-activation
works on both sides of the codex-cli rename (#617)
- caveman-help card shows the Windows config path (%APPDATA%) (#723)
- caveman-parse.js added to HOOK_FILES, opencode payload (.cjs), and the
regenerated checksums.sha256; manifest now matches shipped hook
contents — release must bump PINNED_REF to a tag containing these files
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ySX6TBWZuvFze4ajf7Hpf
/caveman-stats only reported gross output savings, hiding the regime where
the ~1,250-token/turn rule injection costs more than compression saves —
the exact case docs/HONEST-NUMBERS.md warns about.
- Est. rule overhead: per-turn injected-rule input cost x turns
(CAVEMAN_RULE_OVERHEAD_TOKENS overrides the default 1250)
- Est. net: saved minus overhead; when negative, says plainly that caveman
cost more than it saved for this workload
- shown only for attributed uniform spans (follows #601 attribution);
history rows without turn counts are excluded from net, never guessed
- statusline suffix stays gross savings, unchanged semantics
Reimplements the idea from PR #718 locally. 46 tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ySX6TBWZuvFze4ajf7Hpf
- extract mode parsing into src/hooks/caveman-parse.js, consumed by both
the Claude tracker and the opencode plugin — fixes the three #602 drifts
(brevity triggers missing, bogus level overwrote flag with default,
independent modes unreachable via expanded templates)
- unwrap Claude Code's <command-name>/<command-args> slash envelope: real
slash-UI /caveman <level> and /caveman off were silent no-ops (#537);
foreign envelopes skip natural-language detection entirely
- SessionStart branches on payload source: startup resets to configured
default, resume/clear/compact preserve a valid existing flag (#691)
- scheduled-task prompts (<scheduled-task marker) skip flag mutation and
reinforcement so unattended runs aren't hijacked
- per-turn reinforcement honors repo-local defaultMode off via
getDefaultMode(cwd) gate — read-only, never deletes the shared flag
(#634; rejects #532's cross-session flag deletion)
- reinforcement anchor shrunk ~57%, opencode line kept identical (#660)
- statusline setup nudge shown once, gated by .caveman-nudge-shown (#661)
- /caveman-stats delivered via hookSpecificOutput.additionalContext so the
macOS desktop app renders it (#618)
- safeWriteFlag: retry rename on Windows sharing violations, always unlink
temp in finally — no more .caveman-active.<pid>.<ts> litter (#511#578)
- statusline.sh exits 0 on empty suffix file — non-zero exit was hiding
the whole status bar (#711)
- cavecrew-model-overrides resolves plugin root across layouts; env model
overrides were a silent no-op (#645)
- opencode dev-tree loader: base require on the loaded file so
caveman-parse's relative require resolves in both layouts
Supersedes PRs #623#674#700#691#634#660#661#692#632#622#657#578#511#645#590#498#501 with local implementations.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ySX6TBWZuvFze4ajf7Hpf
Replace the ~50-65% range and the ~50%-vs-terse figure with a single 65%
(the measured average output reduction vs verbose baseline) on every product
surface: README hero + ASCII card + benchmark blurb, plugin.json,
marketplace.json, gemini-extension.json, caveman SKILL.md + skill README,
caveman-init rule frontmatter, and the docs site (index.html telemetry widget
+ marquee still showed the old 75%). Trim HONEST-NUMBERS.md to match. Raw
benchmark/eval snapshots and the eval-harness methodology docs are untouched.
Bump PINNED_REF v1.9.0 -> v1.9.1 so the standalone hook-download fallback
fetches cavecrew-model-overrides.js from the release tag instead of 404ing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0163QczkBHKpYgxx3sBc7X3b
/caveman-stats credited ALL output tokens since session start to whatever
mode the flag held at stats time: activating caveman late inflated the
estimate (verbose tokens back-computed as compressed), deactivating it
zeroed honest savings — and the wrong number compounded into the lifetime
history and statusline.
The flag files carried no timestamps, so past sessions cannot be exactly
reconstructed. Fix going forward + honest fallback:
- caveman-config: recordModeChange() appends {ts, mode, prev} to
.caveman-mode-log.jsonl on every actual flag transition (deduped,
symlink-safe via appendFlag, best-effort)
- mode tracker + SessionStart activate hook log every flag mutation
(set, off, NL deactivation, one-shot restore, session-start reset)
- caveman-stats joins the log timestamps against the session JSONL
message timestamps and computes savings per mode span; the first
row's prev covers the pre-inception span
- with no log coverage: a flag written mid-session means the earlier
tokens have UNKNOWN mode — they are excluded and labeled, never
guessed (no-fake-savings); with no evidence of a mid-session change,
whole-session attribution stays (correct when the mode never changed)
- history rows, --share, and the statusline suffix all use the
attributed figure; mixed sessions render a per-mode breakdown with
the estimate basis stated
Old inflated history rows cannot be retroactively corrected (the data to
re-attribute them was never recorded).
Tests: mid-session activation (inflation case), mid-session deactivation
(zeroed case), tracker transition-log dedup, unattributable-prefix
exclusion. Checksums manifest refreshed for the four changed hook files.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kmm2umRGb5nLxdimrwweZ
The files array omitted spawn-options.js, which index.js requires at
startup — the next npm publish would have shipped a package that crashes
with MODULE_NOT_FOUND on every launch (verified via npm pack --dry-run).
- add spawn-options.js to package.json files
- bump 0.1.1 (registry 0.1.0 is stale; the fix is unpublishable without
a version bump anyway — NOT published here)
- add a static packaging test that walks every relative require reachable
from the package entry points (bin + main) and fails if any resolved
module is missing from files
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kmm2umRGb5nLxdimrwweZ
The limit-headroom meter (8a5ab60) printed saved/(saved+output) as
'Session budget saved: ~X% of your usage this session' and 'Est. budget
saved: ~X% of tracked usage'. That ratio is algebraically the output
reduction (always ~65% in full mode) with an output-only denominator —
input + cache tokens, which dominate agentic sessions and count against
Pro/Max limits, were excluded. docs/HONEST-NUMBERS.md on this same
branch says real session-level totals land ~14-21% and below zero on
terse workloads, so the label overstated limit relief.
Fix: say only what the math computes.
- Session view: drop the budget line; the saved line now reads
'(~X% of output)' and the footer states input/cache usage is
unchanged.
- Lifetime view: relabel to 'Est. output reduction: ~X% (output tokens
only, est.)'.
- budgetSavedPct -> outputReductionPct with a comment forbidding
usage/budget relabeling; tests assert no usage/budget claim appears.
- Hook checksum refreshed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kmm2umRGb5nLxdimrwweZ
README headline now states the measured range with method (65% avg vs
verbose default from benchmarks/, ~50% median vs 'Answer concisely.'
control from evals/) plus explicit caveat: input tokens untouched,
session-level savings smaller. Stats box shows output vs input split,
drops the unmeasured ~3x speed row. Same ~75% claim corrected in
SKILL.md description (+ synced plugin copy and dist/caveman.skill),
plugin.json, marketplace.json, gemini-extension.json, caveman-init
rule frontmatter, skill README, and CLAUDE.md.
Note for maintainer: the GitHub repo description still says 'cuts 65%
of tokens' — should read 'cuts ~50-65% of output tokens (measured)'.
Not changeable from a local commit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kmm2umRGb5nLxdimrwweZ
Claude Code Pro/Max users don't spend dollars, they spend a 5-hour /
weekly usage limit. /caveman-stats now also reports the savings in
their currency: 'Session budget saved: ~X% of your usage this session
(est.)', and the lifetime view gets 'Est. budget saved: ~X% of tracked
usage (est.)'.
Honesty rules baked in:
- % = saved / (saved + used) from tokens we actually count — nothing
else. budgetSavedPct returns null (line omitted) when no savings are
measured: honest zero, no claim.
- no plan-limit sizes are assumed or hardcoded (Anthropic doesn't
publish token quotas); the footer says so explicitly.
- clearly labeled (est.); USD lines stay for API users.
Checksums refreshed for the caveman-stats.js change. 5 new tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kmm2umRGb5nLxdimrwweZ
commands/caveman-init.toml told every installed Codex/Gemini user to run
node src/tools/caveman-init.js — a path that only exists inside the
caveman clone, so the command failed with Cannot find module for
everyone but caveman developers.
- toml prompt now gates the repo-relative path on the file existing and
falls back to the standalone script via curl | node -
- src/tools/caveman-init.js: the documented curl|node stdin path was
silently a no-op — require.main is undefined under node -, so the
require.main === module guard never ran main() (exit 0, no output,
no files). Guard now also matches module.id === '[stdin]'.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kmm2umRGb5nLxdimrwweZ
PR #560 shipped the regression test but its source change never made it
into the diff — the branch only adds tests/test_mode_tracker_stdin.js.
Applied the one-line silent-exit error listener the PR describes and
took the author's test verbatim.
Co-authored-by: ousamabenyounes <ousamabenyounes@users.noreply.github.com>
Natural-language matching (#598):
- Deactivation computed first, word-order tolerant: 'turn caveman mode
off' used to ACTIVATE caveman (and reset the level to default),
'turn caveman off' was a no-op.
- 'enable caveman and stop apologizing' no longer deactivates (the old
stop-guard fired on 'stop' anywhere, then the deactivation regex
matched 'caveman and stop').
- Questions ('what is caveman mode?') no longer arm the mode.
- 'normal mode' deactivates only as a command or with caveman context
('how do I exit vim normal mode' no longer kills the session mode).
- Prompts normalized to one line so multiline input matches.
- Scoped brevity ('be brief in the summary section') is a one-off
instruction, not a session-wide switch.
One-shot modes (#599):
- /caveman-commit|-review|-compress save the displaced prose mode to
.caveman-active.prev and the next ordinary prompt restores it (or
deactivates if caveman wasn't active before) — SKILL.md's 'level
persist until changed or session end' holds again.
- Plugin-namespaced /caveman:caveman-commit and -review recognized
(only compress and stats had the variant).
- Deactivation clears the saved prev so nothing resurrects the mode.
Fixes#598, fixes#599
Hook lives at <plugin_root>/src/hooks/ but read SKILL.md via a single
'..' — resolving to nonexistent src/skills/, so every plugin install
silently fell back to the stale hardcoded ruleset (missing language
preservation, no-self-reference, intensity table).
Resolve via candidates in order: $CLAUDE_PLUGIN_ROOT/skills/,
__dirname/../../skills/ (plugin + repo layout), __dirname/../skills/
(standalone $CLAUDE_CONFIG_DIR layout). Sync the two missing rules
into the fallback for installs with no SKILL.md at all.
Fixes#587, fixes#589
withProtectedSegments built sentinels in a single pass and restored them
in a single pass. When two PROTECTED_PATTERNS matched the same span
(e.g. path rule swallows STARTER/BUSINESS, then function-call rule
swallows the resulting type ( 0 )), the outer sentinel restored to
"type ( 0 )" but the inner " 0 " was never substituted back — enum
values reaching the model became literal "( 0 )".
Restore now loops with MAX_RESTORE_PASSES bound; same depth is reached
on the actual #444 inputs in <=2 passes.
Co-Authored-By: Claude <noreply@anthropic.com>
PR #527 added cavecrew-model-overrides.js with a wrong sha256 (eae3... vs
actual 9f26...) and modified caveman-activate.js without regenerating its
entry. bin/install.js verifies downloaded hooks against this manifest and
aborts on mismatch, so both were latent install-breakers on the remote path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Smoke-tested against real opencode 1.4.0 (the missing verification
flagged in 22f75e3) — the plugin never loaded: opencode runs plugins
inside a compiled Bun binary where require() of on-disk files is
rejected ('require() async module is unsupported') and await import()
of a CJS file returns an empty namespace. caveman-config.cjs is now
evaluated as CommonJS by hand (readFileSync + Function wrapper with a
createRequire shim — built-ins still resolve fine).
Three more real-runtime gaps found and fixed in the same pass:
- session-init flag write now also happens at plugin factory time; in
one-shot 'opencode run' the first session.created publishes before
plugin event dispatch is wired, so the event handler alone missed it
- the TUI expands '/caveman <level>' into the command template before
chat.message fires; the parser now recovers the level from the
expanded 'Activate caveman mode: <level>' text
- the non-interactive run path wraps messages in literal quotes
('"/caveman ultra"'); the parser unwraps symmetric quotes
Verified end-to-end against opencode 1.4.0: plugin loads clean,
session-init writes the flag, /caveman ultra flips it, 'stop caveman'
deletes it, and the reinforcement line shows up in the outgoing LLM
system prompt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Folds in #419 (replace the non-existent session.created/tui.prompt.append hooks with real opencode hooks: event dispatcher for session.created, chat.message for mode parsing, experimental.chat.system.transform for reinforcement; fixes #418/#421), #398 (ship the missing caveman-compress.md command + un-ignore it; fixes #426/#451/#464), and #376 plugin-side (drop %APPDATA% branch). Smoke test rewritten for the new hook shapes. NOTE: not smoke-tested against a real opencode runtime here — verify before release.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Installer fixes: #414 (rename PS1 $Args->$InstallerArgs), #437 (detect Copilot via extension dirs, fixes#336), #395 (--skill '*' instead of --all so -a <agent> is honored, fixes#389), #472 (prune orphaned managed hooks from settings.json, fixes#471), #393 (don't double-wire hooks when the plugin manifest already does, fixes#392), #380 (MCP-shrink off by default, requires an upstream, fixes#474), #376 install-side (opencode uses ~/.config/opencode, drop %APPDATA%), #443 (strip tools: from cavecrew agent copies for opencode, #386), #434 (existsSync guard on command copy), #396 (doc: discover profile slugs via --list).
Security hardening: #261 (pin remote fetch to release tag PINNED_REF=v1.8.2, not moving main) and #262 (SHA-256-verify downloaded hook files against src/hooks/checksums.sha256 before they execute; abort on mismatch). #260 (inspect-before-run note). NOTE: enforcement activates fully once a release tag shipping checksums.sha256 is published and PINNED_REF is bumped; v1.8.2 predates the manifest so downloads there warn-and-proceed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Folds in #429 (repo-local <repo>/.caveman/config.json resolution layer between env and user config; symlink-safe, bounded walk) and #248 intent (recognize 'less tokens'/'be brief'/'be terse' as natural-language caveman activation).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>