Summary
hooks/ArtWorkflowGuard.hook.ts gates Bash calls to the Art skill's Generate.ts tool, requiring --workflow=<name> or --freeform-confirmed. Its detector matches the tool's file-path fragment anywhere in the raw command string, with no check that the command actually invokes the file (ArtWorkflowGuard.hook.ts:53-55):
function isGenerateTsCall(command: string): boolean {
return ART_TOOL_PATH_FRAGMENTS.some((frag) => command.includes(frag));
}
Any command that merely mentions the path — a cp copy, a grep search term, a bun build bundler target, even an echoed comment — gets hard-denied (exit 2) identically to a real image-generation call.
Reproduction
Observed live: bun build skills/Art/Tools/Generate.ts --target=bun --outfile=/dev/null (a syntax check) and a cp copying the file were both blocked before execution. Neither generates an image.
Confirmed against the hook directly (PreToolUse JSON on stdin):
| command |
result |
cp skills/Art/Tools/Generate.ts /tmp/backup/Generate.ts.bak |
blocked (exit 2) — false positive |
bun build skills/Art/Tools/Generate.ts --target=bun --outfile=/dev/null |
blocked (exit 2) — false positive |
grep -rl skills/Art/Tools/Generate.ts . |
blocked (exit 2) — false positive |
echo 'refactoring skills/Art/Tools/Generate.ts tomorrow' |
blocked (exit 2) — false positive |
bun skills/Art/Tools/Generate.ts --prompt "x" (no workflow) |
blocked (exit 2) — correct, by design |
bun skills/Art/Tools/Generate.ts --workflow=Mermaid --prompt "x" |
allowed (exit 0) — correct |
Same defect class as #1335
#1335 reported this identical anywhere-in-string matching defect in the security pipeline's destructive-command patterns; it was closed with a fix that anchored those patterns to actual command shape plus a shell-aware pre-pass. ArtWorkflowGuard has its own from-scratch matcher in a separate hook and appears not to have been covered by that pass.
Notably, a sibling in the same hooks directory already implements the correct approach — hooks/lib/egress-class-core.ts:64-67:
// Require an actual `bun`/`bunx` EXECUTION of the tool (not a mere mention in a
// grep/cat/ls command) to avoid false positives on commands that reference the
// filename without running it.
const GENE_EXEC = /\b(?:bun|bunx)\s+[^|;&\n]*?\bOpenRouter\.ts\b/;
EgressClassGuard.hook.ts, which uses this, is not vulnerable to this bug class.
One more instance of the class, lighter blast radius: CommunicationSkillGuard.hook.ts:41-45 uses word-bounded but unanchored regexes (/gmail\.ts['"]?\s+send\b/), so e.g. echo 'todo: refactor the gmail.ts send path for retries' is blocked (verified, exit 2). Worth fixing in the same pass.
Suggested fix
Anchor isGenerateTsCall() to an actual bun/bunx execution of the path, tolerant of the bun run <path> form documented throughout SKILL.md and every Workflows/*.md (while bun build/install/etc. must NOT match):
const GENERATE_EXEC =
/\b(?:bun|bunx)\b(?:\s+run\b)?(?:\s+-{1,2}\S+)*\s+(?:~\/|\.\/)?(?:\.claude\/)?skills\/Art\/Tools\/Generate\.ts\b/;
function isGenerateTsCall(command: string): boolean {
return GENERATE_EXEC.test(command);
}
Validated against 14 cases — the 6 above, plus bash -c wrapping, && chaining, both documented invocation forms, and known-limitation cases; all matched expectations. (A naive port of GENE_EXEC is not enough: its permissive gap between bun and the path still matches the bun build ... false positive; the token right after bun/bunx must be the path itself or the literal run.)
Trade-off, stated honestly: anchoring can miss a hypothetical direct-shebang execution (./skills/Art/Tools/Generate.ts) or variable-indirected invocation (bun "$SCRIPT") that the substring check happened to catch by accident. Given the hook's own docstring notes it's only "the early-warning layer" (Generate.ts itself enforces --workflow regardless), that seems a reasonable trade against routine false denials on ordinary dev commands. Variable-based paths like ${LIFEOS_SKILL_DIR}/Tools/Generate.ts (used in several Workflow docs) already miss under the current matcher too — a pre-existing gap this change neither introduces nor worsens.
Environment
LifeOS 6.0.5 lineage (live install) + verified against a fresh clone of main.
Happy to open a follow-up PR with this change plus a regression fixture, mirroring the approach used to close #1335.
Summary
hooks/ArtWorkflowGuard.hook.tsgates Bash calls to the Art skill'sGenerate.tstool, requiring--workflow=<name>or--freeform-confirmed. Its detector matches the tool's file-path fragment anywhere in the raw command string, with no check that the command actually invokes the file (ArtWorkflowGuard.hook.ts:53-55):Any command that merely mentions the path — a
cpcopy, agrepsearch term, abun buildbundler target, even an echoed comment — gets hard-denied (exit 2) identically to a real image-generation call.Reproduction
Observed live:
bun build skills/Art/Tools/Generate.ts --target=bun --outfile=/dev/null(a syntax check) and acpcopying the file were both blocked before execution. Neither generates an image.Confirmed against the hook directly (PreToolUse JSON on stdin):
cp skills/Art/Tools/Generate.ts /tmp/backup/Generate.ts.bakbun build skills/Art/Tools/Generate.ts --target=bun --outfile=/dev/nullgrep -rl skills/Art/Tools/Generate.ts .echo 'refactoring skills/Art/Tools/Generate.ts tomorrow'bun skills/Art/Tools/Generate.ts --prompt "x"(no workflow)bun skills/Art/Tools/Generate.ts --workflow=Mermaid --prompt "x"Same defect class as #1335
#1335 reported this identical anywhere-in-string matching defect in the security pipeline's destructive-command patterns; it was closed with a fix that anchored those patterns to actual command shape plus a shell-aware pre-pass.
ArtWorkflowGuardhas its own from-scratch matcher in a separate hook and appears not to have been covered by that pass.Notably, a sibling in the same hooks directory already implements the correct approach —
hooks/lib/egress-class-core.ts:64-67:EgressClassGuard.hook.ts, which uses this, is not vulnerable to this bug class.One more instance of the class, lighter blast radius:
CommunicationSkillGuard.hook.ts:41-45uses word-bounded but unanchored regexes (/gmail\.ts['"]?\s+send\b/), so e.g.echo 'todo: refactor the gmail.ts send path for retries'is blocked (verified, exit 2). Worth fixing in the same pass.Suggested fix
Anchor
isGenerateTsCall()to an actualbun/bunxexecution of the path, tolerant of thebun run <path>form documented throughoutSKILL.mdand everyWorkflows/*.md(whilebun build/install/etc. must NOT match):Validated against 14 cases — the 6 above, plus
bash -cwrapping,&&chaining, both documented invocation forms, and known-limitation cases; all matched expectations. (A naive port ofGENE_EXECis not enough: its permissive gap betweenbunand the path still matches thebun build ...false positive; the token right afterbun/bunxmust be the path itself or the literalrun.)Trade-off, stated honestly: anchoring can miss a hypothetical direct-shebang execution (
./skills/Art/Tools/Generate.ts) or variable-indirected invocation (bun "$SCRIPT") that the substring check happened to catch by accident. Given the hook's own docstring notes it's only "the early-warning layer" (Generate.tsitself enforces--workflowregardless), that seems a reasonable trade against routine false denials on ordinary dev commands. Variable-based paths like${LIFEOS_SKILL_DIR}/Tools/Generate.ts(used in several Workflow docs) already miss under the current matcher too — a pre-existing gap this change neither introduces nor worsens.Environment
LifeOS 6.0.5 lineage (live install) + verified against a fresh clone of
main.Happy to open a follow-up PR with this change plus a regression fixture, mirroring the approach used to close #1335.