Skip to content

feat(hooks): PreToolUse gate enforcing swarm delegation for release ops#201

Open
markmhendrickson wants to merge 1 commit into
mainfrom
feat/swarm-delegation-pretooluse-gate
Open

feat(hooks): PreToolUse gate enforcing swarm delegation for release ops#201
markmhendrickson wants to merge 1 commit into
mainfrom
feat/swarm-delegation-pretooluse-gate

Conversation

@markmhendrickson

Copy link
Copy Markdown
Owner

Why

The "delegate through the swarm — you are the orchestrator, not the workhorse" rule (docs/agents/ateles.md) was prose in a prompt, so a session agent could hand-roll a swarm-owned operation just by not remembering it. That happened: a Neotoma release (v0.18.8) was cut by hand (manual PR/merge/tag/GitHub Release) instead of routing through phoenicurus-release (prepare.py → operator approval → publish.py), leaving publish.py unable to adopt it (its preflight refuses when the tag already exists).

Root cause: no action-time enforcement. The only proactive-routing hook (agent_auto_invocation.py) is suggest-only, prompt-time, and wasn't even wired into settings.json — so nothing intercepts the actual irreversible action.

What

A fail-open PreToolUse hook that DENIES the specific swarm-owned Bash commands with a redirect to the owning pipeline:

  • npm publish → route through phoenicurus-release publish.py
  • gh release create → same
  • git tag vX.Y.Z (creation) → publish.py owns tagging

Properties:

  • Deterministic — regex on the actual command, not topic scoring, so it fires regardless of how the session arrived at the action (the failure mode a prompt-time nudge can't catch).
  • Fail-open — any error exits 0 with no output; never blocks a legitimate action on a hook bug.
  • Escapable — a reviewed one-off carries a # SWARM_OVERRIDE token to bypass.
  • Stdlib-only. Rules are data (one entry per gated surface).

Selftest (--selftest) covers gated / allowed / override / read-only-vs-creation (e.g. git tag --list is not blocked; git tag -a v… is).

Follow-up

This gates the release surface, which is where it bit us. Same pattern extends to other swarm-owned surfaces (e.g. product-code gh pr create → route through the issue→Cicada pipeline) as separate entries once we agree the boundary.

🤖 Generated with Claude Code

The 'delegate through the swarm' rule (docs/agents/ateles.md) was prose in a
prompt, so a session agent could hand-roll a swarm-owned operation just by not
remembering it — which happened: a Neotoma release was cut by hand (manual
PR/merge/tag/GitHub Release) instead of routing through phoenicurus-release
(prepare.py -> operator approval -> publish.py), leaving publish.py unable to
adopt it (its preflight refuses when the tag already exists).

Root cause: no action-time enforcement. The only proactive-routing hook
(agent_auto_invocation.py) is suggest-only, prompt-time, and wasn't even wired
into settings.json. So nothing intercepts the actual irreversible action.

This adds a fail-open PreToolUse hook that DENIES the specific swarm-owned Bash
commands (npm publish, gh release create, git tag vX.Y.Z creation) with a
redirect to the owning pipeline. Deterministic (regex on the command, not topic
scoring, so it fires regardless of how the session got there), escapable via a
'# SWARM_OVERRIDE' token for reviewed one-offs, stdlib-only. Selftest covers
gated/allowed/override/read-only-vs-creation cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Loxia Review 🪶

Verdict: APPROVE

Summary

A focused, well-constructed PreToolUse gate that converts the prose "delegate through the swarm" rule into an action-time deny for the three deterministic release surfaces (npm publish, gh release create, release-version git tag). Fail-open, stdlib-only, escapable via an SWARM_OVERRIDE token, and shipped with a self-test — consistent with the existing hooks in .claude/hooks/.

Findings

  • 🔴 Secrets: none detected — no keys, tokens, IBANs, or PII; the only literal is the SWARM_OVERRIDE control token.
  • 🟡 Scope: focused — exactly the hook + its settings.json wiring, nothing extraneous.
  • 🟢 gitleaks: not needed — no env-var name references (BEARER_TOKEN/API_KEY/etc.) introduced, so no allowlist change required.
  • 🟢 Linting: no issues visible — from __future__ import annotations, PEP 585 generics (list[tuple[...]]), clean structure. Verify it passes ruff locally, but nothing jumps out.
  • 🟢 Pattern: follows the local hook convention (fail-open on any exception, deterministic regex, stdlib-only, --selftest/--command test entry points). T3 daemon startup pattern doesn't apply to a Claude Code lifecycle hook.
  • 🟡 Docs: CLAUDE.md's "Session-integrity hooks" section enumerates exactly three lifecycle hooks (session_start.py, user_prompt_submit.py, stop_finalizer.py). This adds a fourth of a different family (a PreToolUse enforcement gate). Consider a one-line mention so the hook inventory in CLAUDE.md stays accurate — non-blocking.

Notes (non-blocking, worth a follow-up)

  • git tag regex can false-positive on compound commands. The pattern anchors the negative-lookahead exclusion right after git tag, but .*\bv\d+\.\d+\.\d+\b then scans the whole command string. So git tag mytag && echo v1.2.3 or git tag foo; cat CHANGELOG-v1.2.3.md would be denied even though the tag being created isn't a version tag. The self-test only covers single-command cases. Consider anchoring the version match nearer the tag argument (e.g. stop .* at a shell separator) or documenting that the # SWARM_OVERRIDE escape hatch covers this. Low severity since the override exists and mis-fires fail safe (deny, not allow).
  • The OVERRIDE_TOKEN in command bypass matches the substring anywhere (including inside an unrelated string literal). That's the intended, documented tradeoff for a lightweight escape hatch — flagging only for awareness.

Loxia automated review · commit cca19ab

@ateles-agent

Copy link
Copy Markdown
Collaborator

🤖 Lanius — Ateles swarm, PR gate inheritance
BLOCKED

Triage & Gate-Status Board

Parent issue: none found.

  • PR body has no closes #N / fixes #N reference.
  • GitHub closingIssuesReferences for this PR: empty.
  • No matching Neotoma issue entity or open GitHub issue by topic search (checked titles/bodies for "PreToolUse gate", "swarm delegation", "release ops enforcement", "hook enforcement" — no hit).

Without a parent issue, no gate_status was ever initialized for this work — there is nothing to inherit from, so PR gate inheritance cannot be evaluated as clear.

Gate status (pre-impl)

Gate Owner State
pm Pavo unsigned (no parent issue)
ux Accipiter unsigned (no parent issue)
arch Waxwing unsigned (no parent issue)

Resolution paths

  1. Backfill the pipeline — open (or link) a parent issue and run the full issue pipeline (gate init + expectations + Pavo) via trigger_swarm_pr.py issue <n>, then have Pavo/Accipiter/Waxwing sign off pm/ux/arch on that issue's Neotoma entity (gate_status.<gate> → signed_off).
  2. Operator override@markmhendrickson may run /confirm-gates-clear to waive all unsigned pre-impl gates and re-trigger the PR pipeline. Only the operator can issue this command.

Until one of these resolves, pr_review assignment (Vanellus) is withheld.

📎 Neotoma: PR entity ent_3daed78260bdee6fcfc43d8b

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.

2 participants