ktg-plugin-marketplace/plugins/ultraplan-local/hooks/scripts/pre-write-executor.mjs
Kjell Tore Guttormsen c407d3451d feat(voyage)!: rename stats filenames, settings keys, hook prefixes [skip-docs]
- lib/stats/event-emit.mjs: STATS_FILENAME -> trekexecute-stats.jsonl + comment
- hooks/scripts/post-bash-stats.mjs: stats target + comment -> trekexecute-stats.jsonl
- lib/stats/cache-analyzer.mjs: help-text + comment -> trekexecute-stats.jsonl
- tests/lib/stats-event-emit.test.mjs (lines 104, 117): fixture assertions
- settings.json: ultraplan/ultraresearch -> trekplan/trekresearch keys + statsFile values
- tests/lib/doc-consistency.test.mjs: allowlist (line 83) + accessor cfg.ultraplan?.* -> cfg.trekplan?.* (lines 91, 93) — atomic-pair, prevents vacuous undefined assertions
- scripts/q3-cache-prefix-experiment.mjs: STATS_JSONL hardcoded path -> voyage data dir + trekexecute filename
- hooks/scripts/pre-bash-executor.mjs (2 lines), pre-compact-flush.mjs (2 lines), pre-write-executor.mjs (1 line): [ultraplan]/[ultraplan-local] stderr prefix -> [voyage]
- commands + agents/review-orchestrator.md + CLAUDE.md: prose stats filename literals -> trek* equivalents

Atomic per session-spec: settings.json scope keys + doc-consistency.test.mjs
allowlist + property accessors committed together to prevent silent vacuous
undefined-equals-undefined assertions.

Part of voyage-rebrand session 2 (W3.7 / Step 9).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 14:49:03 +02:00

125 lines
3.8 KiB
JavaScript

#!/usr/bin/env node
// Hook: pre-write-executor.mjs
// Event: PreToolUse (Write)
// Purpose: Block writes to security-sensitive paths during plan execution.
//
// Protocol:
// - Read JSON from stdin: { tool_name, tool_input }
// - tool_input.file_path — the target path for Write tool
// - BLOCK (exit 2): writes to security infrastructure, shell configs, secrets
// - Allow (exit 0): everything else
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
const HOME = process.env.HOME || process.env.USERPROFILE || '/tmp';
// ---------------------------------------------------------------------------
// BLOCK rules — path patterns that must never be written during execution.
// ---------------------------------------------------------------------------
const BLOCK_RULES = [
{
name: 'Git hook injection (.git/hooks/)',
test: (p) => /\/\.git\/hooks\//.test(p),
description:
'Writing to .git/hooks/ could inject malicious git hooks that execute ' +
'on every commit, push, or checkout. Blocked.',
},
{
name: 'Claude settings self-modification',
test: (p) => /\/\.claude\/settings[^/]*\.json$/.test(p),
description:
'Writing to .claude/settings.json could disable security hooks or ' +
'change permission modes. Blocked.',
},
{
name: 'Claude hooks self-modification',
test: (p) => /\/\.claude\/hooks\//.test(p) || /\/\.claude-plugin\//.test(p),
description:
'Writing to .claude/hooks/ or .claude-plugin/ could modify security ' +
'hook configuration. Blocked.',
},
{
name: 'Shell configuration files',
test: (p) => {
const sensitive = [
`${HOME}/.zshrc`,
`${HOME}/.bashrc`,
`${HOME}/.bash_profile`,
`${HOME}/.profile`,
`${HOME}/.zshenv`,
`${HOME}/.zprofile`,
];
const resolved = resolve(p);
return sensitive.some((s) => resolved === s || resolved.startsWith(s + '.'));
},
description:
'Writing to shell config files (~/.zshrc, ~/.bashrc, etc.) could inject ' +
'persistent commands. Blocked.',
},
{
name: 'SSH directory',
test: (p) => {
const resolved = resolve(p);
return resolved.startsWith(`${HOME}/.ssh/`) || resolved === `${HOME}/.ssh`;
},
description: 'Writing to ~/.ssh/ could compromise SSH keys or config. Blocked.',
},
{
name: 'AWS credentials',
test: (p) => {
const resolved = resolve(p);
return resolved.startsWith(`${HOME}/.aws/`) || resolved === `${HOME}/.aws`;
},
description: 'Writing to ~/.aws/ could compromise cloud credentials. Blocked.',
},
{
name: 'GnuPG directory',
test: (p) => {
const resolved = resolve(p);
return resolved.startsWith(`${HOME}/.gnupg/`) || resolved === `${HOME}/.gnupg`;
},
description: 'Writing to ~/.gnupg/ could compromise GPG keys. Blocked.',
},
{
name: 'Environment files (.env)',
test: (p) => /\/\.env(?:\.[a-zA-Z0-9]+)?$/.test(p),
description:
'Writing to .env files could expose or modify secrets. Blocked. ' +
'Use .env.template instead.',
},
];
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
let input;
try {
const raw = readFileSync(0, 'utf-8');
input = JSON.parse(raw);
} catch {
// Cannot parse stdin — fail open.
process.exit(0);
}
const filePath = input?.tool_input?.file_path;
if (!filePath || typeof filePath !== 'string') {
process.exit(0);
}
const resolved = resolve(filePath);
for (const rule of BLOCK_RULES) {
if (rule.test(resolved)) {
process.stderr.write(
`[voyage] BLOCKED: ${rule.name}\n` +
` Path: ${resolved}\n` +
` ${rule.description}\n`
);
process.exit(2);
}
}
// Allow
process.exit(0);