chore(voyage): W3 hardening (S2) — exec-form hooks, enforced disallowed-tools, F2 decision
S2 of the 2.1.181 upgrade. Schemas verified verbatim against the official
slash-commands and hooks docs before editing (a first-pass camelCase
'disallowedTools' claim was caught and corrected to kebab-case against the doc).
- CC-14 (SHIP): migrate all 7 hooks in hooks/hooks.json to exec-form
{command:"node", args:["${CLAUDE_PLUGIN_ROOT}/hooks/scripts/X.mjs"]}. Doc
recommends exec-form whenever a hook references a path placeholder; protects
consumers installing under a path with spaces. ${CLAUDE_PLUGIN_ROOT}
interpolates inside args (verified). hooks-json-stop-wired test made
form-agnostic (normalizes command+args to one invocation string).
- CC-11 (SHIP): add `disallowed-tools: Agent, TeamCreate` to trekexecute
frontmatter, enforcing its documented "No Agent tool, no TeamCreate" rule.
allowed-tools grants auto-approval but does NOT remove tools from the pool,
so the prior omission left Agent callable; disallowed-tools removes it.
trekexecute is the only command with a documented exclusion.
- CC-15 (DECIDE: keep universal): re-affirm F2 deferral. pre-bash/pre-write
executors stay universal -- session-agnostic safety (rm -rf /, ~/.ssh, .env)
that narrowing to execute-only would only weaken. Header comments corrected.
- CC-10 (DECIDE: design note, no code): no blanket Agent(model:opus) deny rule
-- would break balanced/economy profiles; any model-enforcement must be
profile-aware, deferred into W2. Folded into open question #3.
Matrix updated with S2 resolutions section. Tests 578 pass / 0 fail / 2 skip;
claude plugin validate passes (only pre-existing root-CLAUDE.md warning).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqBYc8Ltrk7LipyJmGxXiB
This commit is contained in:
parent
3f77b68727
commit
66b3b15fb6
6 changed files with 60 additions and 24 deletions
|
|
@ -4,6 +4,7 @@ description: Disciplined plan executor — single-session or multi-session with
|
|||
argument-hint: "[--project <dir>] [--fg | --resume | --dry-run | --validate | --step N | --session N] [plan.md]"
|
||||
model: opus
|
||||
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
|
||||
disallowed-tools: Agent, TeamCreate
|
||||
---
|
||||
|
||||
# Ultraexecute Local
|
||||
|
|
@ -1606,7 +1607,10 @@ code-path.
|
|||
9. **Progress file is ground truth.** Resume uses the progress file, not git log.
|
||||
|
||||
10. **No sub-agents.** The executor reads and implements directly.
|
||||
No Agent tool, no TeamCreate, no delegation.
|
||||
No Agent tool, no TeamCreate, no delegation. Enforced (not just
|
||||
documented) via `disallowed-tools: Agent, TeamCreate` in the frontmatter,
|
||||
which removes both tools from the pool — `allowed-tools` alone does not,
|
||||
since omission leaves a tool callable (CC 2.1.152).
|
||||
|
||||
11. **Worktree isolation is mandatory for parallel execution.** Every parallel
|
||||
`claude -p` session MUST run in its own git worktree. Never launch two or
|
||||
|
|
|
|||
|
|
@ -58,17 +58,26 @@
|
|||
|
||||
| ID | Change (version) | Type | Voyage relevance | Decision | Rationale |
|
||||
|----|------------------|------|------------------|----------|-----------|
|
||||
| **CC-10** | `Tool(param:value)` permission syntax, e.g. `Agent(model:opus)` (**2.1.178**, verified) | NEW | Voyage pins `opus` in 23 agent frontmatters *by convention*; this could enforce it as a hard permission rule | **EVALUATE** ⚠️ | **Trap:** a blanket `Agent(model:opus)`-style rule conflicts with the `balanced`/`economy` profiles, which deliberately spawn `sonnet`. Any enforcement must be **profile-aware** → tie to W2. Do NOT ship a blanket opus-lock. |
|
||||
| **CC-11** | `disallowed-tools` in command/skill frontmatter (2.1.152, verified) | NEW | tighten per-command tool surfaces (e.g. ensure `trekexecute` can't spawn Agents — it already documents "no Agent tool") | **EVALUATE** (low) | Promote existing *documented* tool exclusions into *enforced* `disallowed-tools`. Small, defense-in-depth. Check interaction with `allowed-tools`. |
|
||||
| **CC-10** | `Tool(param:value)` permission syntax, e.g. `Agent(model:opus)` (**2.1.178**, verified) | NEW | Voyage pins `opus` in 23 agent frontmatters *by convention*; this could enforce it as a hard permission rule | **DECIDED (design note)** ⚠️ | **Trap:** a blanket `Agent(model:opus)`-style rule conflicts with the `balanced`/`economy` profiles, which deliberately spawn `sonnet`. Any enforcement must be **profile-aware** → tie to W2. Do NOT ship a blanket opus-lock. **S2 resolution: no rule added; deferred into W2** — see §S2 resolutions. |
|
||||
| **CC-11** | `disallowed-tools` in command/skill frontmatter (2.1.152, verified) | NEW | tighten per-command tool surfaces (e.g. ensure `trekexecute` can't spawn Agents — it already documents "no Agent tool") | **SHIPPED ✅** | Promote existing *documented* tool exclusions into *enforced* `disallowed-tools`. Small, defense-in-depth. **S2: added `disallowed-tools: Agent, TeamCreate` to `trekexecute` frontmatter** (the only command with a documented exclusion). Verified `allowed-tools` omission does NOT remove a tool from the pool — `disallowed-tools` does. |
|
||||
| **CC-12** | Stop/SubagentStop hooks can return `hookSpecificOutput.additionalContext` without being a hook error (2.1.163) | NEW | `post-compact-flush.mjs` already emits `additionalContext`; SubagentStop is a new surface | **DEFER** | Possible richer continuity injection. No current gap forces it. |
|
||||
| **CC-13** | Stop/SubagentStop hook input now includes `background_tasks` + `session_crons` (2.1.145) | NEW | `otel-export.mjs` (Stop hook) observability | **DEFER** | Could enrich exported telemetry. Adopt when next touching the exporter. |
|
||||
| **CC-14** | Hook `args: string[]` exec-form — no shell, no quoting (2.1.139) | NEW | Voyage's 7 hooks invoke node scripts via shell form with `${CLAUDE_PLUGIN_ROOT}` paths | **EVALUATE** (low) | Exec-form removes a class of path-quoting bugs. Low-risk robustness upgrade across `hooks/hooks.json`; verify each hook's invocation. |
|
||||
| **CC-15** | Hook `if:` conditions for Read/Edit/Write paths now match reliably (2.1.139/176); reopens deferred **F2** (scope pre-bash/pre-write executors to execute sessions) | FIX | `pre-bash-executor.mjs`, `pre-write-executor.mjs` are currently universal | **EVALUATE** | F2 was deferred with "universal protection wins." The `if:` mechanism now works, so the *option* is real again — but the original rationale (brief/plan sessions also benefit from the denylist) may still hold. Re-decide explicitly, don't auto-adopt. |
|
||||
| **CC-14** | Hook `args: string[]` exec-form — no shell, no quoting (2.1.139) | NEW | Voyage's 7 hooks invoke node scripts via shell form with `${CLAUDE_PLUGIN_ROOT}` paths | **SHIPPED ✅** | Exec-form removes a class of path-quoting bugs. **S2: migrated all 7 hooks** in `hooks/hooks.json` to `{command:"node", args:["${CLAUDE_PLUGIN_ROOT}/…"]}`. Official hooks doc recommends exec-form *"whenever the hook references a path placeholder"* — protects consumers who install Voyage under a path containing spaces. `${CLAUDE_PLUGIN_ROOT}` interpolates in `args` (verified). |
|
||||
| **CC-15** | Hook `if:` conditions for Read/Edit/Write paths now match reliably (2.1.139/176); reopens deferred **F2** (scope pre-bash/pre-write executors to execute sessions) | FIX | `pre-bash-executor.mjs`, `pre-write-executor.mjs` are currently universal | **DECIDED (keep universal)** | F2 was deferred with "universal protection wins." The `if:` mechanism now works, so the *option* is real again — but the original rationale holds. **S2 resolution: KEEP UNIVERSAL.** These guardrails (rm -rf /, fork bombs; writes to ~/.ssh, .env, .git/hooks) are session-agnostic safety; narrowing to execute-only would only weaken protection with no benefit and never interferes with brief/research/plan work. Header comments corrected to state the universal scope. |
|
||||
| **CC-16** | `SessionStart` `reloadSkills` + `sessionTitle` (2.1.152) | NEW | Voyage sets session title via `UserPromptSubmit` (`session-title.mjs`) | **SKIP** | Current mechanism works and is command-scoped (title reflects the invoked `/trek*` command). SessionStart-title would fire before the command is known. No gain. |
|
||||
| **CC-17** | Hook `terminalSequence` output — notifications/bells without a TTY (2.1.141); `continueOnBlock` for PostToolUse (2.1.139); `MessageDisplay` event (2.1.152); Stop block-cap 8 (2.1.143) | NEW | minor ergonomics; Voyage hooks are fail-open and non-interactive | **SKIP/DEFER** | No current need. `MessageDisplay`/`continueOnBlock` SKIP (no use case); `terminalSequence` DEFER (could notify on long headless waves). Block-cap is informational. |
|
||||
|
||||
---
|
||||
|
||||
## S2 resolutions (W3 hardening — 2026-06-18)
|
||||
|
||||
S2 shipped the mechanical W3 items and recorded the two W3 decisions. Schemas verified verbatim against the official slash-commands and hooks docs before any edit (the casing of `disallowed-tools` and exec-form `${CLAUDE_PLUGIN_ROOT}` interpolation are both load-bearing; a first-pass assistant claim of `disallowedTools` camelCase was caught and corrected against the doc).
|
||||
|
||||
- **CC-14 — SHIPPED.** All 7 hooks in `hooks/hooks.json` migrated to exec-form (`command:"node"`, `args:["${CLAUDE_PLUGIN_ROOT}/hooks/scripts/X.mjs"]`). `tests/hooks/hooks-json-stop-wired.test.mjs` updated to be form-agnostic (normalizes `command`+`args` to one invocation string). 578/580 pass, 0 fail.
|
||||
- **CC-11 — SHIPPED.** `disallowed-tools: Agent, TeamCreate` added to `commands/trekexecute.md`, promoting its documented "No Agent tool, no TeamCreate, no delegation" rule from prose to enforcement. Key fact: `allowed-tools` grants auto-approval but does **not** remove tools from the pool (doc, slash-commands.md), so the prior omission left Agent callable; `disallowed-tools` removes it. trekexecute is the only command with a documented exclusion (trekplan's "never spawn more agents than warranted" is an adaptive-count guideline, not an exclusion — trekplan legitimately uses Agent).
|
||||
- **CC-15 — DECIDED (keep universal).** F2 stays deferred: `pre-bash-executor.mjs` / `pre-write-executor.mjs` remain universal. Rationale re-affirmed explicitly now that `if:` works — narrowing to execute-only sessions would weaken session-agnostic safety with no benefit. Header comments corrected to state the universal scope by design.
|
||||
- **CC-10 — DECIDED (design note; no code).** Do **not** add a `permissions.deny` rule like `Agent(model:opus)`. Such rules live in `settings.json` permissions (colon separator inside parens, confirmed 2.1.178), not in frontmatter, and a blanket opus-lock would break the `balanced`/`economy` profiles that deliberately spawn `sonnet`. If model-enforcement is ever wanted, it must be **profile-aware** and emitted as part of the W2 model/effort work (S3/S4) — e.g. a profile could ship its own `availableModels`/deny set. Until then, the `model: opus` convention in the 23 agent frontmatters stays advisory. Folded into W2 open question #3.
|
||||
|
||||
## W2 — Model & effort alignment (gates v5.4)
|
||||
|
||||
| ID | Change (version) | Type | Voyage relevance | Decision | Rationale |
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/pre-bash-executor.mjs"
|
||||
"command": "node",
|
||||
"args": ["${CLAUDE_PLUGIN_ROOT}/hooks/scripts/pre-bash-executor.mjs"]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -15,7 +16,8 @@
|
|||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/pre-write-executor.mjs"
|
||||
"command": "node",
|
||||
"args": ["${CLAUDE_PLUGIN_ROOT}/hooks/scripts/pre-write-executor.mjs"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -25,7 +27,8 @@
|
|||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/session-title.mjs"
|
||||
"command": "node",
|
||||
"args": ["${CLAUDE_PLUGIN_ROOT}/hooks/scripts/session-title.mjs"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -36,7 +39,8 @@
|
|||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/post-bash-stats.mjs"
|
||||
"command": "node",
|
||||
"args": ["${CLAUDE_PLUGIN_ROOT}/hooks/scripts/post-bash-stats.mjs"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -46,7 +50,8 @@
|
|||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/pre-compact-flush.mjs"
|
||||
"command": "node",
|
||||
"args": ["${CLAUDE_PLUGIN_ROOT}/hooks/scripts/pre-compact-flush.mjs"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -56,7 +61,8 @@
|
|||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/post-compact-flush.mjs"
|
||||
"command": "node",
|
||||
"args": ["${CLAUDE_PLUGIN_ROOT}/hooks/scripts/post-compact-flush.mjs"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -66,7 +72,8 @@
|
|||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/otel-export.mjs"
|
||||
"command": "node",
|
||||
"args": ["${CLAUDE_PLUGIN_ROOT}/hooks/scripts/otel-export.mjs"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
#!/usr/bin/env node
|
||||
// Hook: pre-bash-executor.mjs
|
||||
// Event: PreToolUse (Bash)
|
||||
// Purpose: Block or warn about destructive shell commands during plan execution.
|
||||
// Purpose: Block or warn about destructive shell commands. Wired universally
|
||||
// (every Voyage session, not just execution) by deliberate decision — these
|
||||
// are session-agnostic safety rails (rm -rf /, fork bombs, …). The CC if:
|
||||
// path-scoping mechanism now works, but narrowing to execute-only would only
|
||||
// weaken protection with no offsetting benefit. See cc-upgrade matrix CC-15/F2.
|
||||
//
|
||||
// Protocol:
|
||||
// - Read JSON from stdin: { tool_name, tool_input }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
#!/usr/bin/env node
|
||||
// Hook: pre-write-executor.mjs
|
||||
// Event: PreToolUse (Write)
|
||||
// Purpose: Block writes to security-sensitive paths during plan execution.
|
||||
// Purpose: Block writes to security-sensitive paths. Wired universally (every
|
||||
// Voyage session, not just execution) by deliberate decision — protecting
|
||||
// ~/.ssh, ~/.aws, .git/hooks, .env, shell configs benefits every session.
|
||||
// The CC if: path-scoping mechanism now works, but narrowing to execute-only
|
||||
// would only weaken protection with no benefit. See cc-upgrade matrix CC-15/F2.
|
||||
//
|
||||
// Protocol:
|
||||
// - Read JSON from stdin: { tool_name, tool_input }
|
||||
|
|
|
|||
|
|
@ -16,6 +16,13 @@ function loadHooksJson() {
|
|||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
// Hooks may use shell form ({command: "node X.mjs"}) or exec form
|
||||
// ({command: "node", args: ["X.mjs"]}, CC 2.1.139). Normalize both to a single
|
||||
// invocation string so assertions are form-agnostic.
|
||||
function invocationOf(h) {
|
||||
return [h.command || '', ...(h.args || [])].join(' ').trim();
|
||||
}
|
||||
|
||||
test('hooks.json — Stop key exists with at least one entry', () => {
|
||||
const cfg = loadHooksJson();
|
||||
assert.ok(cfg.hooks, 'hooks.json mangler top-level "hooks" object');
|
||||
|
|
@ -26,13 +33,13 @@ test('hooks.json — Stop key exists with at least one entry', () => {
|
|||
test('hooks.json — Stop entry refererer otel-export.mjs', () => {
|
||||
const cfg = loadHooksJson();
|
||||
const stopEntries = cfg.hooks.Stop;
|
||||
const allCommands = stopEntries.flatMap((entry) =>
|
||||
(entry.hooks || []).map((h) => h.command || ''),
|
||||
const allInvocations = stopEntries.flatMap((entry) =>
|
||||
(entry.hooks || []).map(invocationOf),
|
||||
);
|
||||
const hasOtelExport = allCommands.some((cmd) => cmd.includes('otel-export.mjs'));
|
||||
const hasOtelExport = allInvocations.some((cmd) => cmd.includes('otel-export.mjs'));
|
||||
assert.ok(
|
||||
hasOtelExport,
|
||||
`ingen Stop-hook refererer otel-export.mjs. Funnet: ${JSON.stringify(allCommands)}`,
|
||||
`ingen Stop-hook refererer otel-export.mjs. Funnet: ${JSON.stringify(allInvocations)}`,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -41,17 +48,18 @@ test('hooks.json — Stop entry bruker ${CLAUDE_PLUGIN_ROOT}-substitusjon', () =
|
|||
const stopEntries = cfg.hooks.Stop;
|
||||
const otelEntry = stopEntries
|
||||
.flatMap((entry) => entry.hooks || [])
|
||||
.find((h) => (h.command || '').includes('otel-export.mjs'));
|
||||
.find((h) => invocationOf(h).includes('otel-export.mjs'));
|
||||
assert.ok(otelEntry, 'fant ikke otel-export-entry i Stop');
|
||||
const otelInvocation = invocationOf(otelEntry);
|
||||
assert.match(
|
||||
otelEntry.command,
|
||||
otelInvocation,
|
||||
/\$\{CLAUDE_PLUGIN_ROOT\}/,
|
||||
'otel-export-command bruker ikke ${CLAUDE_PLUGIN_ROOT}-prefix — relative paths feiler i headless',
|
||||
'otel-export-invocation bruker ikke ${CLAUDE_PLUGIN_ROOT}-prefix — relative paths feiler i headless',
|
||||
);
|
||||
assert.match(
|
||||
otelEntry.command,
|
||||
otelInvocation,
|
||||
/^node\s+/,
|
||||
'otel-export-command starter ikke med "node " — invocation-form ikke korrekt',
|
||||
'otel-export-invocation starter ikke med "node " — invocation-form ikke korrekt',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -60,6 +68,6 @@ test('hooks.json — Stop entry har "type": "command"', () => {
|
|||
const stopEntries = cfg.hooks.Stop;
|
||||
const otelHook = stopEntries
|
||||
.flatMap((entry) => entry.hooks || [])
|
||||
.find((h) => (h.command || '').includes('otel-export.mjs'));
|
||||
.find((h) => invocationOf(h).includes('otel-export.mjs'));
|
||||
assert.equal(otelHook.type, 'command', 'otel-export-hook mangler "type": "command"');
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue