Writes were hardened via safeWriteFlag (PRs #70/#71) but readers still
trusted whatever the flag contained. A local attacker with write access
to ~/.claude/ could symlink the flag at a secret file and have the
per-turn reinforcement inject its bytes into model context, or the
statuslines echo ANSI escapes to the terminal on every keystroke.
- caveman-config.js: new readFlag() — lstat symlink refuse, 64-byte cap,
O_NOFOLLOW, VALID_MODES whitelist. Returns null on any anomaly.
- caveman-mode-tracker.js: per-turn reinforcement routes through
readFlag() instead of fs.readFileSync.
- caveman-statusline.sh / .ps1: symlink + size refuse, strip to
[a-z0-9-], whitelist-validate before rendering.
- compress.py (3 synced copies): is_sensitive_path() denylist refuses
.env*, .netrc, keys/certs, ~/.ssh|.aws|.gnupg|.kube|.docker, and any
basename containing secret/credential/password/apikey/token/privatekey
(separator-insensitive). Fails loudly before read — no silent exfil
of credentials to the Anthropic API.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Parent-chain walk produced false positives: macOS /tmp -> /private/tmp
and symlinked home dirs broke all flag writes. Check only the immediate
parent directory and the target itself — that covers the threat model
(attacker replacing the flag with a symlink) without breaking legit
paths.
Extend safeWriteFlag() in caveman-config.js with:
- hasSymlinkInPath() walks every ancestor component and refuses
if any is a symlink (protects against parent-dir redirect)
- Atomic write via temp file + rename
- O_EXCL on temp open to prevent race
Removes duplicate local helper from mode-tracker.
Co-Authored-By: tuanaiseo <221258316+tuanaiseo@users.noreply.github.com>
Consolidate write protection into safeWriteFlag() helper in
caveman-config.js. Applied to all flag write sites:
- caveman-activate.js (SessionStart)
- caveman-mode-tracker.js slash command
- caveman-mode-tracker.js natural-language activation
Refuses symlink targets via lstat, opens with O_NOFOLLOW where
supported, writes with 0600.
Co-Authored-By: tuanaiseo <221258316+tuanaiseo@users.noreply.github.com>
When ~/.claude/package.json (or any ancestor) contains "type": "module",
Node treats every .js file under that tree as an ES module. The caveman
hooks use require() and crash with:
ReferenceError: require is not defined in ES module scope
surfaced as:
SessionStart:clear hook error / UserPromptSubmit hook error
Failed with non-blocking status code: .../caveman-activate.js:9
This pins the hooks directory to CommonJS via a local package.json, so
module resolution no longer depends on whatever the user's ~/.claude
directory declares. Also wires the new file into install/uninstall
scripts so standalone installs (curl | bash / Invoke-WebRequest)
copy it into ~/.claude/hooks/ alongside the JS files.
Addresses the ESM sub-case flagged in #167 (comment by mrx-arafat).
Does not fix the Windows path-with-spaces expansion issues in #167/#78/#72
which have a separate root cause in plugin.json ${CLAUDE_PLUGIN_ROOT}
quoting.
Both SessionStart and UserPromptSubmit hook commands passed
${CLAUDE_PLUGIN_ROOT} unquoted, so the shell split the expansion
on whitespace whenever a user's plugin root contained a space
(e.g. /Users/Tyler Laprade/...). Node received a truncated path
and errored with "Cannot find module '/Users/Tyler'", preventing
the plugin from loading.
Wrapping the variable in double quotes keeps the path intact on
any POSIX-style shell and has no effect on paths without spaces.
Fixes#157
Fragment targets #caveman-commit / #caveman-review were missing.
Replace skills table with ### subsections, triggers inline on each
blurb; add /caveman:compress trigger on compress lead-in.
Gemini CLI fails to load skills through symlinks. Replace symlinks
in skills/compress/ with actual file copies and update CI to keep
them in sync. Remove redundant root compress/ directory.
Gemini CLI fails to load skills through symlinks. Replace symlinks
in skills/compress/ with actual file copies and update CI to keep
them in sync. Remove redundant root compress/ directory.
Enable repo-local Codex hooks and use the current nested SessionStart
matcher format. Document macOS/Linux auto-start, the Windows hook limit,
and the feature flag needed when copying the hook to other repos.
All hook files hardcoded ~/.claude as the Claude config directory.
Users who set CLAUDE_CONFIG_DIR (e.g. for XDG compliance) had hooks
writing to the wrong location. Now all hooks check CLAUDE_CONFIG_DIR
first, falling back to ~/.claude.
Closes#140
Mirror the /caveman command path: skip writing flag file when
getDefaultMode() returns 'off'. Without this, "talk like caveman"
with CAVEMAN_DEFAULT_MODE=off would write 'off' to the flag file,
making statusline/reinforcement inconsistent.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Independent modes have their own skill behavior — emitting base caveman
rules ("Drop articles, fragments OK") on every turn conflicts with the
specialized format those modes expect.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
README tells users they can say "talk like caveman" to activate, but the
UserPromptSubmit hook only matched /caveman commands. This meant the flag
file and statusline badge stayed out of sync when users activated via
natural language — the model would speak caveman (it reads the prompt)
but the hook never wrote the flag file.
Now matches: "activate caveman", "turn on caveman mode", "talk like
caveman", "disable caveman", "turn off caveman", etc. Uses a negative
guard so "stop caveman" doesn't trigger activation first.
Uses getDefaultMode() for natural language activation to respect
CAVEMAN_DEFAULT_MODE and config.json, same as /caveman command.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The SessionStart hook injects caveman rules once, but models lose them
when other plugins (output styles, learning modes) inject competing
style instructions on every turn. Recency and repetition win in LLM
attention — a one-shot injection loses to per-turn reinforcement.
Emit a short structured reminder via hookSpecificOutput on every user
message when caveman is active. Uses the same JSON format Claude Code
expects from hooks, keeping the full ruleset from SessionStart in
context while this reminder keeps it top-of-mind.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace hardcoded intensity table and examples with runtime loading of skills/caveman/SKILL.md (single source of truth). Strip YAML frontmatter and filter the intensity table and examples to include only the active mode; fall back to a minimal built-in ruleset when SKILL.md is missing (standalone installs). Add INDEPENDENT_MODES (commit, review, compress) to short-circuit and emit a brief activation line for modes that use their own skill files. Normalize the wenyan alias to wenyan-full. Overall simplifies maintenance and prevents duplicated, stale rule text in the hook.
Replace the terse one-line caveman activation message with a full, structured ruleset and examples. Introduces INTENSITY and EXAMPLES maps (lite/full/ultra + wenyan variants), selects the active level from mode with a fallback, and builds a detailed output covering persistence, rules, examples, auto-clarity and boundaries. Rationale: anchor terse response behavior more reliably to prevent drift and provide selectable verbosity/compression profiles while preserving exact code/commit formatting.
Mode tracker wrote getDefaultMode() result to flag file even when it
returned "off". Now guards against it — same pattern as caveman-activate.
Added test coverage for off mode in both activate and tracker paths.
Also: fix swapped step comments in uninstall.sh, update stale CLAUDE.md
description for /caveman default.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Set CAVEMAN_DEFAULT_MODE=off or {"defaultMode":"off"} in config to
skip session-start activation. No flag file written, no rules injected.
User can still manually activate with /caveman.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
/caveman-help displays all modes, skills, triggers, config options,
and deactivation — one-shot display, no mode change.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Symlinks in plugins/caveman/skills/compress/ were checked out as plain
text path stubs on Windows (core.symlinks=false), causing Codex to reject
SKILL.md as missing YAML frontmatter. Replace with verbatim copies of
source files and add CI sync step to keep them in sync.
Closes#92
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Make the installer require a configured statusLine before treating hooks as already installed and ensure PowerShell 5.1 compatibility. hooks/install.ps1: stop using -AsHashtable, adjust hooks access, track presence of settings.statusLine and only skip installation when SessionStart, UserPromptSubmit and statusLine are present. hooks/install.sh: add HAS_STATUSLINE check and update the Node-based settings validation to require settings.statusLine. tests/test_hooks.py: add test_install_reconfigures_missing_statusline to verify installer adds a missing statusLine. tests/verify_repo.py: add a manifest path to the verification list and assert that -AsHashtable is not used in install.ps1 to preserve compatibility with Windows PowerShell 5.1.
Add a PowerShell statusline script (hooks/caveman-statusline.ps1) and update activation/installer logic to support Windows and avoid clobbering custom statuslines. install.sh and install.ps1 now check for a full set of hook files and wired hook entries before short-circuiting, wire a managed statusline path, and prefer not to overwrite user statusLine settings. Uninstall scripts now remove the managed statusline only when it was installed. caveman-activate.js updated to detect existing statusLine entries and emit a platform-appropriate snippet. Add tests (tests/test_hooks.py) and a local verification runner (tests/verify_repo.py) to validate install/uninstall/activation flows and caveman-compress fixtures. Also update README and caveman-compress docs and stats (token savings ~45%→~46%) and clarify auto-activation and statusline behavior.
Make several robustness and clarity fixes across scripts and docs:
- README: Clarify agent-specific behavior (Codex, Cursor/Windsurf) and note Codex uses $caveman and lacks some bundled plugins.
- caveman-compress: Avoid division-by-zero when computing token savings; skip unclosed markdown fences when extracting code blocks to prevent false positives.
- hooks (install/uninstall, PowerShell and shell): Pass settings and hooks paths via environment variables to avoid injection issues with special characters, quote generated command paths, use safer here-strings in PowerShell, and ensure settings.json is written via the resolved path. Also remove installer backup files during uninstall and add explanatory comments.
These changes improve security, correctness, and developer-facing documentation.
Add a new caveman-styled README (CLAUDE.md) documenting project overview, file ownership, CI sync workflow, hook system, skill system, agent distribution, evals, and benchmarks. Also add CLAUDE.original.md as the original, more formal copy for reference/backup.
Add .windsurf/skills/caveman SKILL.md and update the sync workflow to copy and commit the new file. Expand README install docs with agent-specific install instructions and standalone hook usage; update AGENTS.md and GEMINI.md to use the new caveman-compress path. Add Windows PowerShell installers (hooks/install.ps1 and hooks/uninstall.ps1) and enhance shell installers (hooks/install.sh, hooks/uninstall.sh) to support --force, plugin detection, clearer statusline wiring messages, and idempotent JSON merging/removal of hook entries. Provides uninstall guidance for other agents and improves overall install/uninstall robustness.
Add statusline support and installer/uninstaller hooks, improve compression tooling and docs. Key changes:
- Add caveman statusline badge script (hooks/caveman-statusline.sh) and wire it into install/uninstall (hooks/install.sh, hooks/uninstall.sh); installer now requires node, backs up settings.json, and makes the statusline executable.
- Enhance SessionStart hook (hooks/caveman-activate.js) to detect missing statusline config and emit a setup nudge; update hooks README with statusline usage and setup instructions.
- Add gemini-extension.json, AGENTS.md, and GEMINI.md to register skills and support Gemini CLI installation.
- Update README to mention statusline badge, Gemini CLI install, and agent list.
- Improve caveman-compress: rename skill (caveman-compress), clarify CLI path/usage in SKILL.md, return on validation failure without overwriting, bump benchmark encoding, and make compressor robust to LLM outer fences by stripping wrapping fences and expanding max_tokens; add explicit rule to avoid wrapping entire output in fences.
- Strengthen code-block extraction in validate.py to handle variable-length fences (```/~~~), nested fences, and CommonMark rules.
- Minor SKILL.md tweak: resume caveman mode when user asks to clarify or repeats question.
These changes integrate a visible status badge for Claude Code users, improve install/uninstall reliability, extend Gemini support, and make the compressor/validator more robust when interacting with LLM outputs.
The UserPromptSubmit hook similarly writes to `~/.claude/.caveman-active` without checking whether the path is a symlink. This duplicates the same local file-clobber risk in another execution path.
Affected files: caveman-mode-tracker.js
Signed-off-by: tuanaiseo <221258316+tuanaiseo@users.noreply.github.com>
The SessionStart hook writes to `~/.claude/.caveman-active` using `fs.writeFileSync` on a predictable path. If that path is replaced with a symlink, Node will follow it and overwrite the symlink target. A local attacker (or another process running as the same user) could abuse this to modify unintended files writable by the user.
Affected files: caveman-activate.js, caveman-mode-tracker.js
Signed-off-by: tuanaiseo <221258316+tuanaiseo@users.noreply.github.com>
The SessionStart hook writes to `~/.claude/.caveman-active` using `fs.writeFileSync` on a predictable path. If that path is replaced with a symlink, Node will follow it and overwrite the symlink target. A local attacker (or another process running as the same user) could abuse this to modify unintended files writable by the user.
Affected files: caveman-activate.js, caveman-mode-tracker.js
Signed-off-by: tuanaiseo <221258316+tuanaiseo@users.noreply.github.com>