How Claude Code Hooks Integrate with Caveman: Complete Technical Guide
Caveman integrates with Claude Code through three hook entry points—SessionStart, UserPromptSubmit, and Statusline—that handle mode initialization, per-prompt tracking, and visual feedback via shared configuration helpers.
The Caveman project by JuliusBrussee is a Claude Code plugin that brings "caveman-style" prose to every session. Understanding how Claude Code hooks integrate with Caveman reveals a clean architecture for persistent mode tracking across AI coding environments. This guide walks through each hook's implementation, shared utilities, and the Opencode bridge that extends the same behavior to alternative front-ends.
SessionStart Hook: Initializing Caveman Mode
The caveman-activate.js hook runs when Claude Code starts a session. It receives a JSON payload containing session_id, source, and cwd, then orchestrates mode setup and rule injection.
Payload Processing and Source Branching
The hook parses the incoming payload and branches based on the source type:
// src/hooks/caveman-activate.js (lines 23-44, 84-108)
function activate(payload, timedOut) {
let source = timedOut ? 'unknown' : 'startup';
let sessionId = null, sessionCwd;
if (payload) {
const data = JSON.parse(payload);
source = data.source || source;
sessionCwd = data.cwd;
sessionId = validateSessionId(data.session_id);
}
run(source, sessionCwd, sessionId);
}
Sources like startup or clear trigger a mode reset, while resume or compact preserve the previously stored mode.
Mode Persistence and Rule Emission
The core logic (lines 120-138, 210-250) resolves the target mode, persists it with writeSessionMode, and emits the filtered ruleset to stdout:
// Mode resolution: reset vs. continuation
const mode = resolveMode(source, sessionId); // env → config → default 'full'
writeSessionMode(sessionId, mode);
recordModeChange(sessionId, mode, Date.now());
// Emit ruleset as hidden system context
const rules = loadFilteredRuleset(mode);
process.stdout.write(rules);
One-Time Status Line Nudge
If the user has never configured a status-line badge, the hook appends a setup reminder and creates .caveman-nudge-shown to prevent repetition.
UserPromptSubmit Hook: Runtime Mode Control
The caveman-mode-tracker.js hook executes on every user prompt, enabling dynamic mode toggles without restarting Claude Code.
Slash Command and Natural Language Parsing
The hook uses parseModeChange (shared with Opencode) to detect:
- Slash commands:
/caveman,/caveman-lite,/caveman-ultra,/caveman-commit - Natural language triggers: "activate caveman", "stop caveman", etc.
// src/hooks/caveman-mode-tracker.js (lines 70-98)
function handlePrompt(input, sessionId) {
const change = parseModeChange(input, { getDefaultMode, expandedTpl: true });
if (!change) return;
// Apply detected change
if (change.action === 'clear') removeFlag();
else if (change.action === 'set') safeWriteFlag(flagPath, change.mode);
}
Reinforcement Line Injection
For non-independent modes, the hook emits a concise reinforcement line (lines 124-140) to maintain caveman style when competing instructions appear:
CAVEMAN MODE ACTIVE: responding in ultra-primal style
This prevents style drift from other plugins that might inject their own system prompts.
Statusline Hooks: Visual Mode Indicators
The status-line scripts provide visual feedback in Claude Code's interface. They are referenced in settings.json (configured via the SessionStart nudge) rather than called directly by other hooks.
Bash Implementation
# src/hooks/caveman-statusline.sh (lines 16-30)
FLAG_PATH="${HOME}/.caveman-active"
MODE=$(cat "$FLAG_PATH" 2>/dev/null || echo "off")
case "$MODE" in
full) echo "[CAVEMAN]" ;;
ultra) echo "[CAVEMAN:ULTRA]" ;;
lite) echo "[CAVEMAN:LITE]" ;;
commit) echo "[CAVEMAN:COMMIT]" ;;
off) echo "" ;;
*) echo "[CAVEMAN:${MODE^^}]" ;;
esac
A PowerShell counterpart (caveman-statusline.ps1) provides Windows support with identical badge logic.
Shared Configuration Layer
All hooks import caveman-config.js as their single source of truth:
| Helper | Purpose |
|---|---|
getDefaultMode |
Resolution chain: env var → repo config → user config → full |
safeWriteFlag |
Symlink-hardened writes preventing TOCTOU attacks |
readSessionModeRaw / writeSessionMode |
Per-session state management |
gcSessionStore |
Cleanup of stale session entries |
validateSessionId |
Input sanitization |
loadFilteredRuleset |
Load skills/caveman/SKILL.md filtered by intensity |
Each hook resolves this module individually, so a missing sibling degrades gracefully rather than crashing the entire plugin.
Opencode Bridge: Extending Hook Behavior
The src/plugins/opencode/plugin.js bridge adapts Caveman's Claude Code hooks for the Opencode environment, which uses different event names but the identical core logic.
Event Mapping
| Opencode Event | Equivalent Claude Code Hook | Handler |
|---|---|---|
session.created |
SessionStart | handleSessionCreated |
chat.message |
UserPromptSubmit | parseModeChange on text parts |
experimental.chat.system.transform |
(reinforcement line) | System prompt injection |
Runtime Compatibility
Since Opencode uses Bun, the plugin loads CJS helpers via a new Function wrapper (lines 61-73):
// src/plugins/opencode/plugin.js (excerpt)
const loadCjs = (code) => new Function('module', 'exports', 'require', code);
const configModule = { exports: {} };
loadCjs(fs.readFileSync('./caveman-config.cjs', 'utf8'))(configModule, configModule.exports, require);
const { getDefaultMode, safeWriteFlag, parseModeChange } = configModule.exports;
This ensures symlink-safe flag operations and identical mode-state logic across both front-ends.
Integration Flow Summary
Claude Code Session Start
↓
caveman-activate.js ──→ Write mode flag ──→ Emit filtered ruleset ──→ Status-line nudge
↓
User submits prompt ──→ caveman-mode-tracker.js ──→ Parse command/NL ──→ Update flag ──→ Reinforce style
↓
Status line scripts ──→ Read flag ──→ Render badge [CAVEMAN] / [CAVEMAN:ULTRA] / etc.
The Opencode bridge mirrors this flow using its native event system while reusing the same configuration and parsing helpers.
Summary
-
Three hooks handle Claude Code integration:
caveman-activate.js(SessionStart),caveman-mode-tracker.js(UserPromptSubmit), and status-line scripts for visual feedback. -
Shared configuration in
caveman-config.jsprovides mode resolution, safe I/O, and rule filtering to all hooks with graceful degradation. -
Runtime toggles via slash commands or natural language update the mode flag immediately without session restart.
-
Reinforcement lines prevent style drift when other plugins inject competing instructions.
-
Opencode compatibility is achieved through a thin bridge that maps native events to the same core handlers using runtime CJS loading.
Frequently Asked Questions
What triggers the Caveman SessionStart hook?
Claude Code invokes caveman-activate.js automatically when a session begins, passing a JSON payload with session_id, source, and cwd. The hook initializes the mode based on the source type—resetting for startup/clear or preserving state for resume/compact sources.
How does Caveman handle mode changes during a conversation?
The caveman-mode-tracker.js hook parses every user prompt for slash commands (/caveman, /caveman-ultra, etc.) or natural language triggers through parseModeChange. Detected changes immediately update the session flag via safeWriteFlag or removeFlag, with reinforcement lines emitted to maintain style consistency.
Why does Caveman use separate status-line scripts instead of direct hook calls?
The status-line scripts (caveman-statusline.sh and .ps1) are designed for Claude Code's settings.json integration, not direct hook invocation. They read the mode flag independently and output badges for the UI, decoupling visual feedback from the core hook execution flow.
How does the Opencode plugin reuse Claude Code hook logic?
The Opencode bridge in src/plugins/opencode/plugin.js loads the same caveman-config and caveman-parse helpers via runtime CJS execution, then maps Opencode events (session.created, chat.message, experimental.chat.system.transform) to equivalent handlers—ensuring identical behavior without code duplication.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →