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
|
|
@ -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