Skip to content

ArtWorkflowGuard blocks any Bash command that merely mentions Generate.ts — same anywhere-in-string matching class as #1335 (CommunicationSkillGuard has a lighter variant) #1453

Description

@jbmml

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions