Skip to content

feat(just-recipes): enforce recipes via PreToolUse hook - #58

Draft
drewdrewthis wants to merge 1 commit into
mainfrom
feat/just-recipes-hook
Draft

feat(just-recipes): enforce recipes via PreToolUse hook#58
drewdrewthis wants to merge 1 commit into
mainfrom
feat/just-recipes-hook

Conversation

@drewdrewthis

Copy link
Copy Markdown
Owner

Reviewer cockpit

  • Scariest thing: a buggy PreToolUse Bash hook can deny ALL Bash tool calls machine-wide (a syntax error exits 2, which Claude Code treats as deny). Mitigations: every internal failure path exits 0 (allow), no set -e, bash -n verified, garbage/empty-stdin tested, and a kill switch (JUST_RECIPES_ENFORCE=off|0) that short-circuits before any parsing.
  • Where to click first: plugins/just-recipes/hooks/enforce-just.sh (55 lines), then hooks/hooks.json.
  • Blast radius elsewhere: release-please config/manifest additions only; no other plugin touched.

Why

v0.1 of just-recipes only advises preferring recipes; nothing enforces it. This PR closes the loop: where a justfile resolves, raw bash is denied with a pointer to just --list, the just wrap "<cmd>" escape hatch, and the recipe-authoring habit loop.

What changed

  • Hook enforces instead of advising → alternative: keep prose-only guidance in SKILL.md → consequence: deterministic funneling, at the cost of the machine-wide-deny risk mitigated above.
  • Fail-open on every error path (missing jq/just, unreadable stdin, bad JSON) → alternative: fail-closed → consequence: enforcement can silently lapse, but a bug can never freeze Bash.
  • Justfile probe via just --summary in $CLAUDE_PROJECT_DIR → alternative: walk parents looking for a justfile filename → consequence: honors just's own resolution (fallback, modules) with zero path logic.
  • Tiny read-only allowlist (cd pwd echo ls cat command -v which) → alternative: none → consequence: avoids nuisance denials on trivial inspection.
  • release-please: registered plugins/just-recipes with bump-minor-pre-major + bump-patch-for-minor-pre-major, manifest seeded at 0.2.0 → consequence: first release PR does not jump to 1.0.0. (marketplace.json carries no per-plugin version field — nothing to bump there.)
  • SKILL.md (45 lines): enforcement model, escape hatch + $CODEX_ROOT/state/wrap.log, habit loop / just wrap-report, flat-recipe convention, kill switch; v0.1 discovery/safety content retained.

Human verification

  • In a repo with a justfile and the plugin enabled, a raw touch x Bash call is denied with the recipe-pointer message.
  • just <recipe> and repos without a justfile pass through untouched.
  • JUST_RECIPES_ENFORCE=off claude ... disables enforcement.

How I can prove I was successful

bash -n passed. Standalone matrix (sample hook JSON piped to stdin; scratch dir contains a justfile):

case input decision
just command just --list allow (exit 0, no output)
raw cmd, justfile present npm test deny (JSON below)
raw cmd, no justfile npm test (CLAUDE_PROJECT_DIR=/tmp) allow
kill switch npm test + JUST_RECIPES_ENFORCE=off allow
garbage stdin not json allow (exit 0)
empty stdin allow (exit 0)
allowlist ls -la allow
prefix trap justify things deny (word boundary holds)
escape hatch just wrap "touch x" allow

Deny output (verbatim):

{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Raw bash is funneled through just here. Run 'just --list' to find a recipe, or use the escape hatch: just wrap \"<your command>\". To add a recipe: see the just-recipes skill."}}

Load test: claude --plugin-dir .../plugins/just-recipes -p "Reply with exactly: LOADED"LOADED. claude plugin validate plugins/just-recipes✔ Validation passed. Declared hook path diffed against the file on disk → match.

Live end-to-end (claude -p in a scratch dir with a justfile, plugin loaded via --plugin-dir), model transcript excerpts — green CI is not this proof:

Raw command denied, file NOT created:

Raw bash is funneled through just here. Run 'just --list' to find a recipe, or use
the escape hatch: just wrap "<your command>". To add a recipe: see the just-recipes skill.
...
Note: `denied-probe.txt` was not created.
$ ls denied-probe.txt
ls: denied-probe.txt: No such file or directory

Recipe allowed:

**2. `just default`** — succeeded. Output verbatim:
hi

🤖 Generated with Claude Code

- hooks/enforce-just.sh denies raw Bash where a justfile resolves; allows
  just commands, read-only allowlist, kill switch (JUST_RECIPES_ENFORCE=off),
  missing just/justfile; any internal error fails open (exit 0)
- hooks/hooks.json wires it on PreToolUse matcher Bash
- SKILL.md: enforcement model, escape hatch, habit loop, flat-recipe convention
- version 0.2.0; register in release-please config + manifest seeded at 0.2.0

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 05fb4d78-1321-4c66-a622-f6509cb4f4d4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant