feat(a1): Stop-hook false-completion firewall - #22
Conversation
…out .loop Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wire hooks/stop_firewall.py into .claude-plugin/plugin.json under the
top-level hooks.Stop key (python3 ${CLAUDE_PLUGIN_ROOT}/hooks/stop_firewall.py),
so marketplace installs get the false-completion firewall with zero config.
Adds the manifest-registration acceptance test and the A1 CHANGELOG entry.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…o pytest tmp Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 215b0a1ead
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/stop_firewall.py", |
There was a problem hiding this comment.
Use exec-form args for the hook path
Claude Code runs hook commands without args through a shell, and its hook docs call out ${CLAUDE_PLUGIN_ROOT} paths should be passed via exec-form args or quoted so they remain one argument. If the plugin cache/install path contains spaces or shell-special characters, this command is tokenized before Python starts, so the Stop hook never runs and false Succeeded contracts are allowed; register it as command: "python3" with args: ["${CLAUDE_PLUGIN_ROOT}/hooks/stop_firewall.py"] or quote the placeholder.
Useful? React with 👍 / 👎.
| return [exe] | ||
| root = os.environ.get("CLAUDE_PLUGIN_ROOT", "") | ||
| if root and (Path(root) / "loop" / "__main__.py").is_file(): | ||
| return [sys.executable or "python3", "-m", "loop"] |
There was a problem hiding this comment.
Prevent workspace modules from shadowing the fallback CLI
When there is no loop executable on PATH, this fallback relies on python -m loop plus PYTHONPATH. Hook handlers run in the current project directory, and Python searches that directory before PYTHONPATH, so any target repo with its own top-level loop/ package will execute that project module instead of this plugin's doctor CLI; because the hook fails open, a lying contract in that repo is silently allowed. Invoke the plugin CLI by an absolute path or otherwise ensure the plugin root wins over the workspace.
Useful? React with 👍 / 👎.
PR3 (A1) of the adoption-slices plan (docs/superpowers/plans/2026-07-03-adoption-slices.md).
What
hooks/stop_firewall.py— a Claude Code Stop hook that blocks a session stop with an actionable reason when the cwd's.loop/contract claimsSucceededbutloop doctorreports issues. Safety invariants, each pinned by tests:stop_hook_activeread defensively, blocks at most once per session per doctor-issues digest (tempdir sentinel), sentinel I/O failure errs toward skip..loop/— the doctor subprocess only spawns when a contract exists and claimsSucceeded.contradictory_terminal, so the firewall fires withoutjsonschema/pyyaml.hooks.Stopentry in.claude-plugin/plugin.json(${CLAUDE_PLUGIN_ROOT}, timeout 90; internal subprocess cap 60 fires first), plus a registration test against the real manifest.Review trail
Per-task reviews: both Approved. Whole-branch review: Ready to merge — Yes (empirically confirmed the dep-free structural-fallback path and no self-block on this repo's own dogfood
.loop/); its test-hardening suggestions folded in as215b0a1, re-review approved. Follow-up noted for maintainer: whether_cli_command()should prefer the plugin-root CLI over a PATHloop(plan currently specifies PATH-first).Tests
Canonical suite: 244 passed / 8 skipped (baseline 234/8 at branch; +10 firewall tests). Frontmatter + self_eval gates green.
Post-merge operator step: live-smoke the hook (plugin-cache refresh + Claude Code restart, scaffold a lying terminal, end a session → block appears).
🤖 Generated with Claude Code
https://claude.ai/code/session_01As92NaD7jMDbnQTqXDzBVa