7 Commits
Author SHA1 Message Date
mroberts d3258b224a Route isolated checkouts to jj workspaces
bash-guard mapped 20 mutating git verbs to their jj equivalents but not
`worktree`, so `git worktree add` passed the hook untouched. Claude Code's
built-in EnterWorktree/ExitWorktree tools were a second hole: they create a
git worktree directly, never going through Bash, so the guard never saw them.

A git worktree in a jj repo is not a jj workspace. jj does not manage it, it
never appears in `jj workspace list`, and none of jj's workspace bookkeeping
applies to it -- the isolated checkout ends up outside the VCS that owns the
repo.

Add the `worktree` entry to the git->jj map and a PreToolUse matcher on
EnterWorktree|ExitWorktree that exits 2 with the jj workspace commands on
stderr. The tool matcher replaces a `permissions.deny` entry in user
settings.json: it travels with the plugin and names the replacement command
instead of failing silently.

Read-only `git worktree list` is blocked along with the rest of the verb.
It cannot see jj workspaces, so its empty output reads as "no isolated
checkouts exist" when several do -- worse than a denial.

Verified by running the guard against `git worktree add ../feature` over
socket stdin and confirming both the denial and that the reason names
`jj workspace add`. The new checks fail against the 1.1.1 map.

Tests: 12 passing (bash-guard).
2026-07-28 13:07:00 -05:00
mroberts 58851c8a8c Resolve python tools off PATH, not just as importable modules
1.1.0 reported an installed ruff as missing and skipped the lint. Its
resolution order ended at `python -m <tool>`, guarded by an `import <tool>`
probe, but ruff ships as a standalone Rust binary and is never importable.
The probe failed, the check was recorded as skipped, and the gate reported a
coverage gap that did not exist.

This also regressed 1.0.2, which called a bare `ruff` and let PATH resolve it.
Restore that path, ordered after the project venv and uv so a project-local
tool still wins, and before `python -m` so a binary is found even when a
same-named module is not importable.

Verified by injecting an unused import into audit-terraform and confirming the
guard blocks with `ruff check .`. The new regression test fails against the
1.1.0 resolution order and passes against this one.

Tests: 10 passing (bash-guard), 197 (audit-code), 106 (audit-terraform),
ruff clean across both skills.
2026-07-21 12:30:36 -05:00
mroberts fc4a939482 Gate nested projects in bash-guard instead of passing them ungated
The build-and-test gate searched for marker files only two levels below the
repo root. This repo keeps its python projects at
plugins/<name>/skills/<skill>/, four levels down, so every push reported "no
recognized project layout" and pushed without running a single test. A gate
that announces it did nothing is worse than no gate, because the announcement
scrolls past and the push still succeeds.

Walk to depth 4, and resolve python tooling properly. A bare `ruff` or
`pytest` is usually absent from PATH, and run() treats ENOENT as a pass, so an
unresolved tool would have skipped the check just as silently. Resolution now
tries the project's own .venv, then uv (which needs a [project] table that
audit-terraform does not have), then the interpreter's -m form.

A tool that is genuinely not installed is reported as an advisory note rather
than blocking. Its absence is a gap in coverage, not a defect in the change
being pushed. Silence is the one outcome that is never acceptable.

Verified by injecting a failing test into each skill and confirming the guard
blocks: audit-code resolves through its .venv, audit-terraform through
python -m pytest. The new regression tests fail against the old depth-2 walk
and pass against this one.

Tests: 8 passing (bash-guard), 197 (audit-code), 106 (audit-terraform).
2026-07-21 11:49:48 -05:00
mroberts 600c1fef86 Updated marketplace with the reviews plugin 2026-07-21 10:35:34 -05:00
mroberts 129354cda8 Read hook payload from fd 0, not /dev/stdin
Claude Code delivers the hook payload on a socket. Opening it by path
('/dev/stdin' -> /proc/self/fd/0) fails with ENXIO, so bash-guard.mjs threw on
every invocation and its bare catch exited 0 silently. The guard looked like it
was never dispatched; it was dying on line 29 each time.

readFileSync(0) is read() on the descriptor with no open(), which works on a
socket. The catch now logs instead of swallowing, so this failure mode can never
again masquerade as non-dispatch.

Adds test-bash-guard.py, which drives the guard over a socketpair. A pipe would
not reproduce the bug, so the socket is load-bearing. Verified failing against
the pre-fix guard (silent, no output) and passing after.

Removes the five diagnostic probes and probe.mjs; they served their purpose.
Bumps to 1.0.2 because the plugin cache is keyed by version and an unchanged
version silently skips reinstall.
2026-07-21 10:24:06 -05:00
mroberts 0e9ccaad9f Add hook dispatch probes to isolate why bash-guard never fires
Registers five inert probe entries covering the 2x2 of timeout and
statusMessage on PreToolUse:Bash, plus a PostToolUse statusMessage cell.
Each probe is in its own matcher block so a dropped entry cannot take
the others with it, and logs its identity before reading stdin so a
failed dispatch stays distinguishable from a failed payload read.

The real bash-guard entry is unchanged and serves as the control.
2026-07-20 15:15:49 -05:00
mroberts 391b2a3bc6 Add mroberts plugin marketplace with guards plugin 2026-07-20 14:25:19 -05:00