# How Claude Code Hooks Integrate with Caveman: Complete Technical Guide

> Discover how Claude Code hooks integrate with Caveman. This guide details SessionStart, UserPromptSubmit, and Statusline hooks for mode initialization, prompt tracking, and visual feedback.

- Repository: [Julius Brussee/caveman](https://github.com/JuliusBrussee/caveman)
- Tags: how-to-guide
- Published: 2026-09-06

---

**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`](https://github.com/JuliusBrussee/caveman/blob/main/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:

```js
// 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:

```js
// 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`](https://github.com/JuliusBrussee/caveman/blob/main/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.

```js
// 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`](https://github.com/JuliusBrussee/caveman/blob/main/settings.json) (configured via the SessionStart nudge) rather than called directly by other hooks.

### Bash Implementation

```bash

# 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`](https://github.com/JuliusBrussee/caveman/blob/main/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`](https://github.com/JuliusBrussee/caveman/blob/main/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`](https://github.com/JuliusBrussee/caveman/blob/main/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):

```js
// 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`](https://github.com/JuliusBrussee/caveman/blob/main/caveman-activate.js) (SessionStart), [`caveman-mode-tracker.js`](https://github.com/JuliusBrussee/caveman/blob/main/caveman-mode-tracker.js) (UserPromptSubmit), and status-line scripts for visual feedback.

- **Shared configuration** in [`caveman-config.js`](https://github.com/JuliusBrussee/caveman/blob/main/caveman-config.js) provides 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`](https://github.com/JuliusBrussee/caveman/blob/main/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`](https://github.com/JuliusBrussee/caveman/blob/main/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`](https://github.com/JuliusBrussee/caveman/blob/main/caveman-statusline.sh) and `.ps1`) are designed for Claude Code's [`settings.json`](https://github.com/JuliusBrussee/caveman/blob/main/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`](https://github.com/JuliusBrussee/caveman/blob/main/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.