feat(ultraplan-local): defense-in-depth security hardening for executor
Four-layer security model for ultraexecute-local and headless sessions: Layer 1 — Plugin hooks: pre-bash-executor.mjs (13 BLOCK + 8 WARN rules with bash evasion normalization) and pre-write-executor.mjs (8 path guard rules blocking .git/hooks, .claude/settings, shell configs, .env, SSH/AWS). Layer 2 — Prompt-level security rules: denylist in ultraexecute-local.md Sub-step D and session-spec-template.md Security Constraints section. These are the only rules that work in headless child sessions. Layer 3 — Pre-execution plan validation: new Phase 2.4 scans all Verify and Checkpoint commands against denylist before execution begins. Layer 4 — Replace --dangerously-skip-permissions with scoped --allowedTools "Read,Write,Edit,Bash,Glob,Grep" --permission-mode bypassPermissions in ultraexecute-local.md, headless-launch-template.md, and session-decomposer.md. Blocks Agent, MCP, WebSearch in child sessions. Also adds Hard Rules 14-16: verify command security check, no writing outside repository root, no writing to security-sensitive paths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2e125d9030
commit
aa21e59ac2
7 changed files with 539 additions and 6 deletions
125
plugins/ultraplan-local/hooks/scripts/pre-write-executor.mjs
Normal file
125
plugins/ultraplan-local/hooks/scripts/pre-write-executor.mjs
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
#!/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(
|
||||
`[ultraplan] BLOCKED: ${rule.name}\n` +
|
||||
` Path: ${resolved}\n` +
|
||||
` ${rule.description}\n`
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
// Allow
|
||||
process.exit(0);
|
||||
Loading…
Add table
Add a link
Reference in a new issue