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
|
|
@ -185,8 +185,9 @@ The script must:
|
|||
**Important script conventions:**
|
||||
- Use `#!/usr/bin/env bash` shebang
|
||||
- Use `set -euo pipefail`
|
||||
- Each `claude -p` invocation must use `--dangerously-skip-permissions`. Prepend
|
||||
`unset ANTHROPIC_API_KEY` before each invocation to prevent accidental API billing
|
||||
- Each `claude -p` invocation must use `--allowedTools "Read,Write,Edit,Bash,Glob,Grep"`
|
||||
and `--permission-mode bypassPermissions`. Prepend `unset ANTHROPIC_API_KEY`
|
||||
before each invocation to prevent accidental API billing
|
||||
- Background processes use `&` and are collected with `wait`
|
||||
- PID tracking for wait targets
|
||||
- Exit codes propagated correctly
|
||||
|
|
|
|||
|
|
@ -115,6 +115,82 @@ Steps: {N}
|
|||
{if warnings}: Warnings: {list}
|
||||
```
|
||||
|
||||
## Phase 2.4 — Pre-execution security scan
|
||||
|
||||
**Runs for all modes except dry-run** (dry-run has its own report format).
|
||||
|
||||
Scan every `Verify:` and `Checkpoint:` command in the parsed plan against the
|
||||
executor security denylist. This catches dangerous commands before execution begins.
|
||||
|
||||
### Extract commands
|
||||
|
||||
For each step in the plan, extract the command string from:
|
||||
- `Verify:` field (the shell command after the backtick-quoted command)
|
||||
- `Checkpoint:` field (the git commit command)
|
||||
|
||||
Also extract Exit Condition commands if present.
|
||||
|
||||
### Check against denylist
|
||||
|
||||
For each extracted command, check against these patterns:
|
||||
|
||||
**BLOCK patterns (stop execution immediately):**
|
||||
|
||||
| Pattern | Threat |
|
||||
|---------|--------|
|
||||
| `rm` with both `-r` and `-f` flags (any order) | Recursive force delete |
|
||||
| `chmod 777` or `chmod -R 777` | World-writable permissions |
|
||||
| `curl`/`wget` piped to `bash`/`sh`/`zsh` | Remote code execution |
|
||||
| `eval` with `$`, backtick, or `$(` | Code injection via eval |
|
||||
| `mkfs` or `dd` writing to `/dev/sd*`, `/dev/nvme*`, `/dev/hd*` | Disk destruction |
|
||||
| `shutdown`, `reboot`, `halt`, `poweroff` | System shutdown |
|
||||
| `:(){ :\|:& };:` pattern | Fork bomb |
|
||||
| `base64` piped to `bash`/`sh` | Obfuscated code execution |
|
||||
| `crontab -e` or writing to `/etc/cron*` | Persistence via cron |
|
||||
| `kill -9 -1` or `pkill -9 -1` | Kill all user processes |
|
||||
| `history -c` or truncating `~/.bash_history` | Evidence destruction |
|
||||
|
||||
**WARN patterns (report but continue):**
|
||||
|
||||
| Pattern | Concern |
|
||||
|---------|---------|
|
||||
| `npm install --save`, `pip install`, `cargo add` | Dependency changes during execution |
|
||||
| `git push --force` | History rewrite |
|
||||
| `git reset --hard` | Discard uncommitted changes |
|
||||
|
||||
### Scan output
|
||||
|
||||
For each match:
|
||||
```
|
||||
Security scan: Step {N} — {description}
|
||||
Command: {command}
|
||||
{BLOCKED | WARNING}: {pattern name}
|
||||
```
|
||||
|
||||
**If ANY BLOCK pattern is found:**
|
||||
|
||||
```
|
||||
SECURITY SCAN FAILED: {count} dangerous command(s) found in plan.
|
||||
|
||||
Blocked commands:
|
||||
Step {N}: {command} → {reason}
|
||||
|
||||
This plan contains commands blocked by the executor security policy.
|
||||
The plan may have been tampered with or contain hallucinated dangerous commands.
|
||||
|
||||
Options:
|
||||
1. Review and fix the plan file: {path}
|
||||
2. Use --dry-run to inspect all commands without executing
|
||||
3. Use --fg for interactive execution (hooks provide additional protection)
|
||||
```
|
||||
|
||||
Stop execution. Do NOT continue to Phase 2.5.
|
||||
|
||||
**If only WARN patterns found:** Continue execution but include warnings in the
|
||||
pre-execution summary. Report them in the final output under "Security advisories."
|
||||
|
||||
**If clean:** Report `Security scan: PASS ({N} commands checked)` and continue.
|
||||
|
||||
## Phase 2.5 — Execution strategy decision
|
||||
|
||||
Determine how to execute this plan:
|
||||
|
|
@ -321,7 +397,8 @@ Worktree created: session-{N} → {WORKTREE_PATH} (branch: {BRANCH_NAME})
|
|||
For each session N in the wave:
|
||||
```bash
|
||||
cd "$WORKTREE_PATH" && claude -p "/ultraexecute-local --session {N} {plan-path}" \
|
||||
--dangerously-skip-permissions \
|
||||
--allowedTools "Read,Write,Edit,Bash,Glob,Grep" \
|
||||
--permission-mode bypassPermissions \
|
||||
> "$LOG_DIR/session-{N}.log" 2>&1 &
|
||||
```
|
||||
|
||||
|
|
@ -646,6 +723,31 @@ Read the step's `Files:` and `Changes:` fields. Implement exactly as described.
|
|||
|
||||
#### Sub-step D — Verification
|
||||
|
||||
**Security check (mandatory):** Before running the Verify command, check it against
|
||||
the executor security denylist. If the command matches ANY of these patterns,
|
||||
**refuse to execute** — treat as `On failure: escalate` regardless of the plan's
|
||||
On failure setting:
|
||||
|
||||
- `rm -rf` or `rm -fr` with any path
|
||||
- `chmod 777` or `chmod -R 777`
|
||||
- Pipe-to-shell: `curl ... | bash`, `wget ... | sh`, `base64 ... | bash`
|
||||
- `eval` with variable expansion: `eval $VAR`, `eval $(cmd)`, `` eval `cmd` ``
|
||||
- `mkfs`, `dd` writing to block devices (`/dev/sd*`, `/dev/nvme*`)
|
||||
- `shutdown`, `reboot`, `halt`, `poweroff`
|
||||
- Fork bomb patterns
|
||||
- `crontab` writes, `/etc/cron*` modifications
|
||||
- `kill -9 -1` or `pkill -9 -1` (kill all processes)
|
||||
- `history -c` or truncating `~/.bash_history`
|
||||
|
||||
If matched:
|
||||
1. Do NOT execute the command
|
||||
2. Set step status = "failed"
|
||||
3. Log: `SECURITY: Verify command blocked — matches executor denylist: {pattern name}`
|
||||
4. Apply `On failure: escalate` regardless of the plan's On failure setting
|
||||
5. Include in final report under a "Security blocks" section
|
||||
|
||||
If the command passes the security check, run it:
|
||||
|
||||
Run the `Verify:` command exactly as written, via Bash.
|
||||
|
||||
**Rules:**
|
||||
|
|
@ -877,3 +979,19 @@ Never let stats failures block the workflow.
|
|||
`--no-ff`. If any merge produces a conflict, run `git merge --abort`,
|
||||
report the conflicting files, and do not attempt further merges. Never use
|
||||
`--force` or `--strategy-option theirs/ours` to silently resolve conflicts.
|
||||
|
||||
14. **Verify command security check.** Before executing any `Verify:` or
|
||||
`Checkpoint:` command, check it against the executor security denylist
|
||||
(Sub-step D). If the command matches a blocked pattern, escalate
|
||||
immediately — do not execute, do not retry.
|
||||
|
||||
15. **No writing outside the repository.** During step execution, never write
|
||||
files outside the git repository root (`git rev-parse --show-toplevel`).
|
||||
Exception: `.claude/` paths for plans, progress files, and stats.
|
||||
This prevents escape-from-repo attacks where a plan step modifies home
|
||||
directory or system files.
|
||||
|
||||
16. **No writing to security-sensitive paths.** Never write to `.git/hooks/`
|
||||
(git hook injection), `~/.ssh/`, `~/.aws/`, `~/.gnupg/`, `.env` files,
|
||||
shell configs (`~/.zshrc`, `~/.bashrc`, `~/.profile`), or
|
||||
`.claude/settings.json` / `.claude/hooks/` (infrastructure self-modification).
|
||||
|
|
|
|||
24
plugins/ultraplan-local/hooks/hooks.json
Normal file
24
plugins/ultraplan-local/hooks/hooks.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/pre-bash-executor.mjs"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/pre-write-executor.mjs"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
247
plugins/ultraplan-local/hooks/scripts/pre-bash-executor.mjs
Normal file
247
plugins/ultraplan-local/hooks/scripts/pre-bash-executor.mjs
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
#!/usr/bin/env node
|
||||
// Hook: pre-bash-executor.mjs
|
||||
// Event: PreToolUse (Bash)
|
||||
// Purpose: Block or warn about destructive shell commands during plan execution.
|
||||
//
|
||||
// Protocol:
|
||||
// - Read JSON from stdin: { tool_name, tool_input }
|
||||
// - tool_input.command — the shell command string
|
||||
// - BLOCK (exit 2): catastrophic/irreversible operations
|
||||
// - WARN (exit 0): risky but recoverable operations — advisory to stderr
|
||||
// - Allow (exit 0): everything else
|
||||
//
|
||||
// Based on llm-security's pre-bash-destructive.mjs with executor-specific additions.
|
||||
// bash-normalize logic copied inline (MIT) — cannot import from separate plugin.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bash normalization (from llm-security/scanners/lib/bash-normalize.mjs)
|
||||
// Strips bash evasion techniques: empty quotes, ${} expansion, backslash splitting.
|
||||
// ---------------------------------------------------------------------------
|
||||
function normalizeBashExpansion(cmd) {
|
||||
if (!cmd || typeof cmd !== 'string') return cmd || '';
|
||||
|
||||
let result = cmd
|
||||
// Strip empty single quotes: w''get -> wget
|
||||
.replace(/''/g, '')
|
||||
// Strip empty double quotes: r""m -> rm
|
||||
.replace(/""/g, '')
|
||||
// Single-char ${x} -> x (evasion: c${u}rl -> curl, assumes x=x)
|
||||
.replace(/\$\{(\w)\}/g, '$1')
|
||||
// Multi-char ${ANYTHING} -> '' (unknown value, strip entirely)
|
||||
.replace(/\$\{[^}]*\}/g, '')
|
||||
// Strip backtick subshell with empty/whitespace content
|
||||
.replace(/`\s*`/g, '');
|
||||
|
||||
// Iteratively strip backslash between word chars (c\u\r\l needs 2 passes)
|
||||
let prev;
|
||||
do {
|
||||
prev = result;
|
||||
result = result.replace(/(\w)\\(\w)/g, '$1$2');
|
||||
} while (result !== prev);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BLOCK rules — exit 2, command is not executed.
|
||||
// ---------------------------------------------------------------------------
|
||||
const BLOCK_RULES = [
|
||||
{
|
||||
name: 'Filesystem root/home destruction (rm -rf /)',
|
||||
// Matches rm with both -r and -f flags targeting /, ~, or $HOME.
|
||||
// Uses (?:\s|$) instead of \b because / and ~ are non-word chars.
|
||||
pattern: /\brm\s+(?:-[a-zA-Z]*f[a-zA-Z]*\s+|--force\s+)*-[a-zA-Z]*r[a-zA-Z]*\s+(?:\/|~|\$HOME)(?:\s|$)/,
|
||||
description:
|
||||
'`rm -rf /`, `rm -rf ~`, and `rm -rf $HOME` would destroy the filesystem ' +
|
||||
'or home directory. Unconditionally blocked.',
|
||||
},
|
||||
{
|
||||
name: 'World-writable chmod (chmod 777)',
|
||||
pattern: /\bchmod\s+(?:-[a-zA-Z]+\s+)*777\b/,
|
||||
description:
|
||||
'`chmod 777` grants full read/write/execute to all users. ' +
|
||||
'Use minimal permissions (e.g. 644, 755).',
|
||||
},
|
||||
{
|
||||
name: 'Pipe-to-shell (curl|bash, wget|sh)',
|
||||
pattern: /(?:curl|wget)\b[^|]*\|\s*(?:bash|sh|zsh|ksh|dash)\b/,
|
||||
description:
|
||||
'Piping remote content into a shell allows arbitrary remote code execution. ' +
|
||||
'Download first, review, then execute.',
|
||||
},
|
||||
{
|
||||
name: 'Fork bomb',
|
||||
pattern: /:\(\)\s*\{\s*:\s*\|\s*:&\s*\}\s*;?\s*:/,
|
||||
description: 'Fork bomb — exhausts system process resources. Blocked.',
|
||||
},
|
||||
{
|
||||
name: 'Filesystem format (mkfs)',
|
||||
pattern: /\bmkfs(?:\.[a-z0-9]+)?\s/,
|
||||
description: '`mkfs` formats a filesystem, destroying all data. Blocked.',
|
||||
},
|
||||
{
|
||||
name: 'Raw disk overwrite via dd',
|
||||
pattern: /\bdd\b[^&|;]*\bof=\/dev\/(?:sd|nvme|hd|vd|xvd|mmcblk)[a-z0-9]*/,
|
||||
description: '`dd` writing to a raw block device destroys disk data. Blocked.',
|
||||
},
|
||||
{
|
||||
name: 'Direct device write (> /dev/sd*)',
|
||||
pattern: />\s*\/dev\/(?:sd|nvme|hd|vd|xvd|mmcblk)[a-z0-9]*/,
|
||||
description: 'Shell redirection to a block device destroys disk data. Blocked.',
|
||||
},
|
||||
{
|
||||
name: 'eval with variable/command expansion',
|
||||
pattern: /\beval\s+(?:`|\$[\({]|"[^"]*\$)/,
|
||||
description:
|
||||
'`eval` with variable or command substitution is a code injection vector. ' +
|
||||
'Refactor to use explicit commands.',
|
||||
},
|
||||
// --- Executor-specific additions ---
|
||||
{
|
||||
name: 'System shutdown/reboot',
|
||||
pattern: /\b(?:shutdown|reboot|halt|poweroff)\b/,
|
||||
description: 'System shutdown/reboot commands are blocked during execution.',
|
||||
},
|
||||
{
|
||||
name: 'Cron persistence',
|
||||
pattern: /\bcrontab\b|>\s*\/etc\/cron/,
|
||||
description:
|
||||
'Writing to crontab or /etc/cron* creates persistent scheduled tasks. ' +
|
||||
'Blocked during execution.',
|
||||
},
|
||||
{
|
||||
name: 'Base64-encoded execution',
|
||||
pattern: /\bbase64\b[^|]*\|\s*(?:bash|sh|zsh)\b/,
|
||||
description: 'Base64-decoded content piped to shell is obfuscated code execution. Blocked.',
|
||||
},
|
||||
{
|
||||
name: 'Kill all processes (kill -9 -1)',
|
||||
pattern: /\b(?:kill|pkill)\s+-9\s+-1\b/,
|
||||
description: 'Killing all user processes with signal 9. Blocked.',
|
||||
},
|
||||
{
|
||||
name: 'History destruction',
|
||||
pattern: /\bhistory\s+-c\b|>\s*~\/\.bash_history\b|>\s*~\/\.zsh_history\b/,
|
||||
description: 'Clearing shell history or truncating history files. Blocked.',
|
||||
},
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WARN rules — exit 0 with advisory message on stderr.
|
||||
// ---------------------------------------------------------------------------
|
||||
const WARN_RULES = [
|
||||
{
|
||||
name: 'Force push (git push --force)',
|
||||
pattern: /\bgit\s+push\b[^|&;]*(?:--force|-f)\b/,
|
||||
description:
|
||||
'WARNING: `git push --force` rewrites remote history. Prefer `--force-with-lease`.',
|
||||
},
|
||||
{
|
||||
name: 'Hard reset (git reset --hard)',
|
||||
pattern: /\bgit\s+reset\s+--hard\b/,
|
||||
description:
|
||||
'WARNING: `git reset --hard` permanently discards uncommitted changes.',
|
||||
},
|
||||
{
|
||||
name: 'Recursive remove (rm -rf, non-root)',
|
||||
pattern: /\brm\s+(?:-[a-zA-Z]*f[a-zA-Z]*\s+|--force\s+)*-[a-zA-Z]*r[a-zA-Z]*\s+/,
|
||||
description:
|
||||
'WARNING: `rm -rf` permanently deletes files. Verify the target path.',
|
||||
},
|
||||
{
|
||||
name: 'Docker system prune',
|
||||
pattern: /\bdocker\s+system\s+prune\b/,
|
||||
description:
|
||||
'WARNING: `docker system prune` removes all stopped containers and unused images.',
|
||||
},
|
||||
{
|
||||
name: 'npm publish',
|
||||
pattern: /\bnpm\s+publish\b/,
|
||||
description:
|
||||
'WARNING: `npm publish` releases a package to the public registry.',
|
||||
},
|
||||
{
|
||||
name: 'DROP TABLE or DROP DATABASE (SQL)',
|
||||
pattern: /\bDROP\s+(?:TABLE|DATABASE|SCHEMA)\b/i,
|
||||
description:
|
||||
'WARNING: SQL DROP permanently deletes database objects.',
|
||||
},
|
||||
{
|
||||
name: 'DELETE without WHERE (SQL)',
|
||||
pattern: /\bDELETE\s+FROM\s+\w+(?:\s*;|\s*$)/i,
|
||||
description:
|
||||
'WARNING: DELETE FROM without WHERE deletes all rows.',
|
||||
},
|
||||
// --- Executor-specific additions ---
|
||||
{
|
||||
name: 'Dependency installation during execution',
|
||||
pattern: /\b(?:npm\s+install\s+--save|pip3?\s+install\s+(?!-e\s+\.)|cargo\s+add)\b/,
|
||||
description:
|
||||
'WARNING: Installing dependencies during plan execution is unusual. ' +
|
||||
'Verify this is intentional.',
|
||||
},
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Normalize: strip ANSI, collapse whitespace
|
||||
// ---------------------------------------------------------------------------
|
||||
function normalizeCommand(cmd) {
|
||||
return cmd
|
||||
.replace(/\x1B\[[0-9;]*m/g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
let input;
|
||||
try {
|
||||
const raw = readFileSync(0, 'utf-8');
|
||||
input = JSON.parse(raw);
|
||||
} catch {
|
||||
// Cannot parse stdin — fail open.
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const command = input?.tool_input?.command;
|
||||
|
||||
if (!command || typeof command !== 'string') {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Strip bash evasion, then normalize whitespace
|
||||
const deobfuscated = normalizeBashExpansion(command);
|
||||
const normalized = normalizeCommand(deobfuscated);
|
||||
|
||||
// Check BLOCK rules first
|
||||
for (const rule of BLOCK_RULES) {
|
||||
if (rule.pattern.test(normalized)) {
|
||||
process.stderr.write(
|
||||
`[ultraplan] BLOCKED: ${rule.name}\n` +
|
||||
` Command: ${normalized.slice(0, 200)}${normalized.length > 200 ? '...' : ''}\n` +
|
||||
` ${rule.description}\n`
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
// Check WARN rules (advisory — still exit 0)
|
||||
const warnings = [];
|
||||
for (const rule of WARN_RULES) {
|
||||
if (rule.pattern.test(normalized)) {
|
||||
warnings.push(` [WARN] ${rule.name}: ${rule.description}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (warnings.length > 0) {
|
||||
process.stderr.write(
|
||||
`[ultraplan] SECURITY ADVISORY: Potentially risky command.\n` +
|
||||
` Command: ${normalized.slice(0, 200)}${normalized.length > 200 ? '...' : ''}\n` +
|
||||
warnings.join('\n') + '\n'
|
||||
);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
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);
|
||||
|
|
@ -62,7 +62,8 @@ echo "Worktree created: session-{n} (branch: ultraplan/{slug}/session-{n})"
|
|||
|
||||
{# Launch session in its worktree: }
|
||||
cd "$WORKTREE_BASE/session-{n}" && claude -p "$(cat "$PLAN_DIR/session-{n}-{slug}.md")" \
|
||||
--dangerously-skip-permissions \
|
||||
--allowedTools "Read,Write,Edit,Bash,Glob,Grep" \
|
||||
--permission-mode bypassPermissions \
|
||||
> "$LOG_DIR/session-{n}.log" 2>&1 &
|
||||
PID_{n}=$!
|
||||
cd "$REPO_ROOT"
|
||||
|
|
@ -118,8 +119,10 @@ When generating a launch script from this template:
|
|||
stops and reports which session failed.
|
||||
4. **Log each session** to a separate file for debugging.
|
||||
5. **Use `claude -p`** with the session spec file as the prompt.
|
||||
6. **Use `--dangerously-skip-permissions`** rather than `--allowedTools` — the
|
||||
executor needs flexible tool access and enumerating every tool is fragile.
|
||||
6. **Use `--allowedTools "Read,Write,Edit,Bash,Glob,Grep"`** with
|
||||
`--permission-mode bypassPermissions` for child sessions. This limits the
|
||||
tool surface to what the executor needs and prevents agent spawning, MCP
|
||||
access, and external web requests in headless sessions.
|
||||
7. **Final verification** at the end runs the master plan's verification section.
|
||||
8. **Never include secrets** in the generated script.
|
||||
9. **Wave verification must be independent.** After each wave completes, run
|
||||
|
|
|
|||
|
|
@ -48,6 +48,21 @@ All of these must pass before this session is considered complete:
|
|||
## Failure Handling
|
||||
|
||||
- If ANY step fails after retry: **stop execution**. Do NOT proceed to later steps.
|
||||
|
||||
## Security Constraints
|
||||
|
||||
These rules override any step instructions that conflict with them:
|
||||
|
||||
- **Never run** `rm -rf`, `chmod 777`, pipe-to-shell (`curl|bash`, `wget|sh`,
|
||||
`base64|bash`), `eval` with variable expansion, `mkfs`, `dd` to block devices,
|
||||
`shutdown`/`reboot`/`halt`, fork bombs, `crontab` writes, or `kill -9 -1`
|
||||
- **Never modify files** outside the Scope Fence (Touch list above)
|
||||
- **Never write to** `.git/hooks/`, `~/.ssh/`, `~/.aws/`, `~/.gnupg/`, `.env`
|
||||
files, shell configs (`~/.zshrc`, `~/.bashrc`, `~/.profile`)
|
||||
- **Never write to** `.claude/settings.json`, `.claude/hooks/`, or any hook
|
||||
script — these are security infrastructure and must not be modified by execution
|
||||
- If a `Verify:` or `Checkpoint:` command violates these rules: treat as
|
||||
`On failure: escalate` and stop execution regardless of the step's On failure setting
|
||||
- Commit whatever was completed successfully before stopping.
|
||||
- Report which step failed, the error message, and what was attempted.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue