Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 0 additions & 57 deletions .claude/hooks/bash-chain-guard.js

This file was deleted.

44 changes: 21 additions & 23 deletions .claude/hooks/caveman-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,25 @@
// - %APPDATA%\caveman\config.json (Windows fallback)
// 3. 'full'

const fs = require('fs');
const path = require('path');
const os = require('os');
const fs = require("fs");
const path = require("path");
const os = require("os");

const VALID_MODES = [
'off', 'lite', 'full', 'ultra',
'wenyan-lite', 'wenyan', 'wenyan-full', 'wenyan-ultra',
'commit', 'review', 'compress'
];
// biome-ignore format: the whole mode list reads best on one line
const VALID_MODES = ["off", "lite", "full", "ultra", "wenyan-lite", "wenyan", "wenyan-full", "wenyan-ultra", "commit", "review", "compress"];

function getConfigDir() {
if (process.env.XDG_CONFIG_HOME) {
return path.join(process.env.XDG_CONFIG_HOME, 'caveman');
return path.join(process.env.XDG_CONFIG_HOME, "caveman");
}
if (process.platform === 'win32') {
return path.join(
process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
'caveman'
);
if (process.platform === "win32") {
return path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "caveman");
}
return path.join(os.homedir(), '.config', 'caveman');
return path.join(os.homedir(), ".config", "caveman");
}

function getConfigPath() {
return path.join(getConfigDir(), 'config.json');
return path.join(getConfigDir(), "config.json");
}

function getDefaultMode() {
Expand All @@ -46,7 +40,7 @@ function getDefaultMode() {
// 2. Config file
try {
const configPath = getConfigPath();
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
if (config.defaultMode && VALID_MODES.includes(config.defaultMode.toLowerCase())) {
return config.defaultMode.toLowerCase();
}
Expand All @@ -55,7 +49,7 @@ function getDefaultMode() {
}

// 3. Default
return 'full';
return "full";
}

// Symlink-safe flag file write.
Expand Down Expand Up @@ -86,17 +80,21 @@ function safeWriteFlag(flagPath, content) {
try {
if (fs.lstatSync(flagPath).isSymbolicLink()) return;
} catch (e) {
if (e.code !== 'ENOENT') return;
if (e.code !== "ENOENT") return;
}

const tempPath = path.join(flagDir, `.caveman-active.${process.pid}.${Date.now()}`);
const O_NOFOLLOW = typeof fs.constants.O_NOFOLLOW === 'number' ? fs.constants.O_NOFOLLOW : 0;
const O_NOFOLLOW = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW;
let fd;
try {
fd = fs.openSync(tempPath, flags, 0o600);
fs.writeSync(fd, String(content));
try { fs.fchmodSync(fd, 0o600); } catch (e) { /* best-effort on Windows */ }
try {
fs.fchmodSync(fd, 0o600);
} catch (e) {
/* best-effort on Windows */
}
} finally {
if (fd !== undefined) fs.closeSync(fd);
}
Expand Down Expand Up @@ -130,15 +128,15 @@ function readFlag(flagPath) {
if (st.isSymbolicLink() || !st.isFile()) return null;
if (st.size > MAX_FLAG_BYTES) return null;

const O_NOFOLLOW = typeof fs.constants.O_NOFOLLOW === 'number' ? fs.constants.O_NOFOLLOW : 0;
const O_NOFOLLOW = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
const flags = fs.constants.O_RDONLY | O_NOFOLLOW;
let fd;
let out;
try {
fd = fs.openSync(flagPath, flags);
const buf = Buffer.alloc(MAX_FLAG_BYTES);
const n = fs.readSync(fd, buf, 0, MAX_FLAG_BYTES, 0);
out = buf.slice(0, n).toString('utf8');
out = buf.slice(0, n).toString("utf8");
} finally {
if (fd !== undefined) fs.closeSync(fd);
}
Expand Down
101 changes: 58 additions & 43 deletions .claude/hooks/caveman-mode-tracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,69 +2,79 @@
// caveman — UserPromptSubmit hook to track which caveman mode is active
// Inspects user input for /caveman commands and writes mode to flag file

const fs = require('fs');
const path = require('path');
const os = require('os');
const { getDefaultMode, safeWriteFlag, readFlag } = require('./caveman-config');
const fs = require("fs");
const path = require("path");
const os = require("os");
const { getDefaultMode, safeWriteFlag, readFlag } = require("./caveman-config");

const claudeDir = path.join(process.env.PROJECT_ROOT_DIR || os.homedir(), '.claude');
const flagPath = path.join(claudeDir, '.caveman-active');
const claudeDir = path.join(process.env.PROJECT_ROOT_DIR || os.homedir(), ".claude");
const flagPath = path.join(claudeDir, ".caveman-active");

let input = '';
process.stdin.on('data', chunk => { input += chunk; });
process.stdin.on('end', () => {
let input = "";
process.stdin.on("data", (chunk) => {
input += chunk;
});
process.stdin.on("end", () => {
try {
const data = JSON.parse(input);
const prompt = (data.prompt || '').trim().toLowerCase();
const prompt = (data.prompt || "").trim().toLowerCase();

// Natural language activation (e.g. "activate caveman", "turn on caveman mode",
// "talk like caveman"). README tells users they can say these, but the hook
// only matched /caveman commands — flag file and statusline stayed out of sync.
if (/\b(activate|enable|turn on|start|talk like)\b.*\bcaveman\b/i.test(prompt) ||
/\bcaveman\b.*\b(mode|activate|enable|turn on|start)\b/i.test(prompt)) {
if (
/\b(activate|enable|turn on|start|talk like)\b.*\bcaveman\b/i.test(prompt) ||
/\bcaveman\b.*\b(mode|activate|enable|turn on|start)\b/i.test(prompt)
) {
if (!/\b(stop|disable|turn off|deactivate)\b/i.test(prompt)) {
const mode = getDefaultMode();
if (mode !== 'off') {
if (mode !== "off") {
safeWriteFlag(flagPath, mode);
}
}
}

// Match /caveman commands
if (prompt.startsWith('/caveman')) {
if (prompt.startsWith("/caveman")) {
const parts = prompt.split(/\s+/);
const cmd = parts[0]; // /caveman, /caveman-commit, /caveman-review, etc.
const arg = parts[1] || '';
const arg = parts[1] || "";

let mode = null;

if (cmd === '/caveman-commit') {
mode = 'commit';
} else if (cmd === '/caveman-review') {
mode = 'review';
} else if (cmd === '/caveman-compress' || cmd === '/caveman:caveman-compress') {
mode = 'compress';
} else if (cmd === '/caveman' || cmd === '/caveman:caveman') {
if (arg === 'lite') mode = 'lite';
else if (arg === 'ultra') mode = 'ultra';
else if (arg === 'wenyan-lite') mode = 'wenyan-lite';
else if (arg === 'wenyan' || arg === 'wenyan-full') mode = 'wenyan';
else if (arg === 'wenyan-ultra') mode = 'wenyan-ultra';
if (cmd === "/caveman-commit") {
mode = "commit";
} else if (cmd === "/caveman-review") {
mode = "review";
} else if (cmd === "/caveman-compress" || cmd === "/caveman:caveman-compress") {
mode = "compress";
} else if (cmd === "/caveman" || cmd === "/caveman:caveman") {
if (arg === "lite") mode = "lite";
else if (arg === "ultra") mode = "ultra";
else if (arg === "wenyan-lite") mode = "wenyan-lite";
else if (arg === "wenyan" || arg === "wenyan-full") mode = "wenyan";
else if (arg === "wenyan-ultra") mode = "wenyan-ultra";
else mode = getDefaultMode();
}

if (mode && mode !== 'off') {
if (mode && mode !== "off") {
safeWriteFlag(flagPath, mode);
} else if (mode === 'off') {
try { fs.unlinkSync(flagPath); } catch (e) {}
} else if (mode === "off") {
try {
fs.unlinkSync(flagPath);
} catch (e) {}
}
}

// Detect deactivation — natural language and slash commands
if (/\b(stop|disable|deactivate|turn off)\b.*\bcaveman\b/i.test(prompt) ||
/\bcaveman\b.*\b(stop|disable|deactivate|turn off)\b/i.test(prompt) ||
/\bnormal mode\b/i.test(prompt)) {
try { fs.unlinkSync(flagPath); } catch (e) {}
if (
/\b(stop|disable|deactivate|turn off)\b.*\bcaveman\b/i.test(prompt) ||
/\bcaveman\b.*\b(stop|disable|deactivate|turn off)\b/i.test(prompt) ||
/\bnormal mode\b/i.test(prompt)
) {
try {
fs.unlinkSync(flagPath);
} catch (e) {}
}

// Per-turn reinforcement: emit a structured reminder when caveman is active.
Expand All @@ -78,17 +88,22 @@ process.stdin.on('end', () => {
// If the flag is missing, corrupted, oversized, or a symlink pointing at
// something like ~/.ssh/id_rsa, readFlag returns null and we emit nothing
// — never inject untrusted bytes into model context.
const INDEPENDENT_MODES = new Set(['commit', 'review', 'compress']);
const INDEPENDENT_MODES = new Set(["commit", "review", "compress"]);
const activeMode = readFlag(flagPath);
if (activeMode && !INDEPENDENT_MODES.has(activeMode)) {
process.stdout.write(JSON.stringify({
hookSpecificOutput: {
hookEventName: "UserPromptSubmit",
additionalContext: "CAVEMAN MODE ACTIVE (" + activeMode + "). " +
"Drop articles/filler/pleasantries/hedging. Fragments OK. " +
"Code/commits/security: write normal."
}
}));
process.stdout.write(
JSON.stringify({
hookSpecificOutput: {
hookEventName: "UserPromptSubmit",
additionalContext:
"CAVEMAN MODE ACTIVE (" +
activeMode +
"). " +
"Drop articles/filler/pleasantries/hedging. Fragments OK. " +
"Code/commits/security: write normal.",
},
}),
);
}
} catch (e) {
// Silent fail
Expand Down
35 changes: 11 additions & 24 deletions .claude/settings/hooks.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@
"type": "command",
"command": "sh -c 'SESSION=$(jq -r \".session_id\" 2>/dev/null); DIR=\"/tmp/claude-$(id -u)\"; mkdir -p \"$DIR\"; printf \"%s\" \"$(git rev-parse --show-toplevel)\" > \"$DIR/.claude_project_root_${SESSION}\"'",
"timeout": 5,
"statusMessage": "Caching project root...",
"statusMessage": "Caching project root..."
},
{
"type": "command",
"command": "node \"$(git rev-parse --show-toplevel)/.claude/hooks/caveman-activate.js\"",
"timeout": 5,
"statusMessage": "Loading caveman mode...",
},
],
},
"statusMessage": "Loading caveman mode..."
}
]
}
],
"UserPromptSubmit": [
{
Expand All @@ -35,23 +35,10 @@
"type": "command",
"command": "sh -c 'INPUT=$(cat); SESSION=$(printf \"%s\" \"$INPUT\" | jq -r \".session_id\" 2>/dev/null); ROOT=$(cat \"/tmp/claude-$(id -u)/.claude_project_root_${SESSION}\"); printf \"%s\" \"$INPUT\" | PROJECT_ROOT_DIR=\"$ROOT\" node \"$ROOT/.claude/hooks/caveman-mode-tracker.js\"'",
"timeout": 5,
"statusMessage": "Tracking caveman mode...",
},
],
},
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "sh -c 'INPUT=$(cat); SESSION=$(printf \"%s\" \"$INPUT\" | jq -r \".session_id\" 2>/dev/null); ROOT=$(cat \"/tmp/claude-$(id -u)/.claude_project_root_${SESSION}\"); printf \"%s\" \"$INPUT\" | node \"$ROOT/.claude/hooks/bash-chain-guard.js\"'",
"timeout": 5,
"statusMessage": "Checking for command chaining...",
},
],
},
],
},
"statusMessage": "Tracking caveman mode..."
}
]
}
]
}
}
20 changes: 10 additions & 10 deletions .claude/settings/models.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
* =====================================================================================================
*/
{
"availableModels": [
"fable",
"sonnet",
"opus",
"haiku",
"claude-opus-5",
"claude-opus-4-8",
"claude-sonnet-5",
"claude-sonnet-4-6"
]
"availableModels": [
"fable",
"sonnet",
"opus",
"haiku",
"claude-opus-5",
"claude-opus-4-8",
"claude-sonnet-5",
"claude-sonnet-4-6"
]
}
4 changes: 2 additions & 2 deletions .claude/settings/permissions/additional-dirs.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
// Additional directories that Claude Code can access
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"additionalDirectories": ["/tmp", "/workspaces"],
},
"additionalDirectories": ["/tmp", "/workspaces"]
}
}
Loading