@stackmemoryai/stackmemory
Version:
Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.
158 lines (141 loc) • 3.43 kB
JavaScript
/**
* Claude Code pre-tool-use hook for auto-backgrounding commands
*
* Install: Add to ~/.claude/settings.json hooks.pre_tool_use
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
const CONFIG_PATH = path.join(
os.homedir(),
'.stackmemory',
'auto-background.json'
);
const DEFAULT_CONFIG = {
enabled: true,
timeoutMs: 5000,
alwaysBackground: [
'npm install',
'npm ci',
'yarn install',
'pnpm install',
'bun install',
'npm run build',
'yarn build',
'pnpm build',
'cargo build',
'go build',
'make',
'npm test',
'npm run test',
'yarn test',
'pytest',
'jest',
'vitest',
'cargo test',
'docker build',
'docker-compose up',
'docker compose up',
'git clone',
'git fetch --all',
'npx tsc',
'tsc --noEmit',
'eslint .',
'npm run lint',
],
neverBackground: [
'vim',
'nvim',
'nano',
'less',
'more',
'top',
'htop',
'echo',
'cat',
'ls',
'pwd',
'cd',
'which',
'git status',
'git diff',
'git log',
],
verbose: false,
};
function loadConfig() {
try {
if (fs.existsSync(CONFIG_PATH)) {
return {
...DEFAULT_CONFIG,
...JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')),
};
}
} catch {}
return DEFAULT_CONFIG;
}
function shouldAutoBackground(command, config) {
if (!config.enabled) return false;
const cmd = command.trim().toLowerCase();
// Never background these
for (const pattern of config.neverBackground) {
if (cmd.startsWith(pattern.toLowerCase())) return false;
}
// Always background these
for (const pattern of config.alwaysBackground) {
if (cmd.startsWith(pattern.toLowerCase())) return true;
}
return false;
}
// Read hook input from stdin
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => (input += chunk));
process.stdin.on('end', () => {
try {
const hookData = JSON.parse(input);
const { tool_name, tool_input } = hookData;
// Only process Bash tool
if (tool_name !== 'Bash') {
// Allow other tools through unchanged
console.log(JSON.stringify({ permissionDecision: 'allow' }));
return;
}
const command = tool_input?.command;
if (!command) {
console.log(JSON.stringify({ permissionDecision: 'allow' }));
return;
}
// Already backgrounded
if (tool_input.run_in_background === true) {
console.log(JSON.stringify({ permissionDecision: 'allow' }));
return;
}
const config = loadConfig();
if (shouldAutoBackground(command, config)) {
if (config.verbose) {
console.error(
`[auto-bg] Backgrounding: ${command.substring(0, 60)}...`
);
}
// Modify the tool input to add run_in_background using correct schema
console.log(
JSON.stringify({
hookEventName: 'PreToolUse',
permissionDecision: 'allow',
updatedInput: {
...tool_input,
run_in_background: true,
},
})
);
} else {
console.log(JSON.stringify({ permissionDecision: 'allow' }));
}
} catch (err) {
// On error, allow the command through unchanged
console.error('[auto-bg] Error:', err.message);
console.log(JSON.stringify({ permissionDecision: 'allow' }));
}
});