Skip to content

Replace python3-based hooks with a native devkit-engine guard subcommand #65

Description

@5uck1ess

Problem

Both hooks/devkit-guard.sh and hooks/devkit-stop-guard.sh depend on python3 being on PATH to parse session.json. PR #64 centralised this in hooks/lib/read-session.sh, but the underlying dependency is still there and carries real costs:

  • Windows: python3 is not guaranteed. Cold start can blow the hook's 5s timeout (PR fix(hooks): enforce workflow progression on prompt steps + orphan recovery #64 bumped from 2s just to hedge this). Timeout = silent hook skip = unguarded.
  • Minimal containers: bare Alpine, distroless-style setups often ship without python3.
  • Latency: the guard runs on every PreToolUse. A Python subprocess per call is 50-150ms of overhead that compounds fast during high-churn steps.
  • Fragility: fail-closed path (exit 2 if python3 unavailable) means a broken python install hard-blocks all tool calls during a workflow.

Proposal

Add a devkit-engine guard subcommand in Go that the hooks shell out to. The engine binary is already on PATH during plugin runtime, already has lib.ReadSessionJSON, already has the session lock code, and ships cross-platform via the release Makefile (linux/darwin/windows).

Shape

devkit-engine guard --tool-name <name> [--stop]
  • Reads \$CLAUDE_PLUGIN_DATA/session.json via lib.ReadSessionJSON (respects the flock, so no torn reads).
  • Applies the exact same allowlist logic currently in devkit-guard.sh (command/prompt × hard/soft × stale TTL).
  • Exit codes mirror the PreToolUse contract: 0 = allow, 2 = block (with diagnostic on stderr).
  • --stop flag switches to the Stop-hook JSON verdict format (`{"decision":"approve|block","reason":"..."}`) for devkit-stop-guard.sh replacement.

Hook shrinks to a one-liner

#!/usr/bin/env bash
exec devkit-engine guard --tool-name \"\$(jq -r .tool_name)\"

(Or parse tool name in Go by reading stdin directly — eliminates jq too.)

Wins

  • Zero python3 dependency.
  • ~10× faster (single Go process, no interpreter warm-up).
  • Single source of truth for allowlist logic — no drift between Go engine and bash hook.
  • Unit-testable in Go rather than via a bash fixture harness.
  • Timeout can drop back to 2s with margin.

Acceptance criteria

  • New cmd/guard.go subcommand with `--tool-name` and `--stop` flags.
  • `devkit-guard.sh` and `devkit-stop-guard.sh` reduced to thin `exec` wrappers (or removed entirely if the plugin manifest can point directly at the binary).
  • `hooks/lib/read-session.sh` deleted — no longer needed.
  • `hooks/devkit-guard_test.sh` fixture matrix ported to Go table-driven tests against the new subcommand.
  • `hooks.json` timeout returned to `2`.
  • Cross-platform smoke test: macOS, Linux, Windows all pass the fixture matrix.

Out of scope

Context

Split out from PR #64 (issue #63) to keep that PR focused on the behaviour change. Everything in #64 is correct and the hooks work today — this is a robustness / portability upgrade.

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