guard-kit

Permission-friction reduction for coding-agent sessions. A PreToolUse guard decides at call time — block with a corrective message, steer to a better form, rewrite to the allowlisted spelling, auto-allow the provably safe, or log the fall-through — a scanner ranks what the allowlist failed to cover, a curation tool finds the redundant local overrides, and a close-stage triage step makes the whole loop a habit.

Why: a command no allowlist entry matches is decided out of band — by interrupting a human, or by a model asked to judge the call — and that decision is invisible to the agent either way, so it cannot notice, count, or fix the friction it causes. The cost is paid per call, out of the operator’s attention or out of latency and tokens, and compounds as the command surface grows. The kit closes the loop by making the fall-through set — exactly the commands nothing granted — the one thing that is recorded. See SPEC.md for the framework, the generic ruleset, what the steering buys, and the triage criterion.

This file ships in the installer payload; what that payload withholds, and where this page’s SPEC.md links then resolve, is gate-sdk/SPEC.md §Consumer payload.

Most of what guard-kit ships is not a gate: its surfaces are hook members of the gate binary and templates. It registers exactly one, which holds the door binding its own steer messages and its settings templates depend on — and, over the surfaces GUARD_KIT_DOOR_ROOTS names, holds a door on your own pages to a declared audience.

Install

Vendor the kit beside gate-sdk, then:

  1. Copy the config template into your gates dir (default scripts/):

    cp guard-kit/templates/guard-config.knobs scripts/guard-config.knobs
    

    Register the gate it ships in your gates.list:

    check-door-binding
    

    It resolves through gate-sdk’s registry path (your gates dir first, then each kit’s checks/), and the pre-commit hook runs it when its # graph: manifest triggers, read at commit.

    No guard script is copied: the shell guard, the optional wakeup-guard and escalation-guard are binary arms, wired by pointing a hook’s command field at the gate binary GATE_SDK_NATIVE_BIN names, under <project-dir>, with --hook shell-guard as its args (or --hook wakeup-guard, --hook escalation-guard). <project-dir> is your harness’s project-directory variable, which templates/settings-hooks.json spells for Claude Code; the template carries the binary as a placeholder you replace at the merge, as its note says (guard-kit/SPEC.md §The shell guard). Where the binary is absent the harness cannot start the hook, lets the call through, and the guard steers nothing.

    Your project’s own block/steer/allow rules are a command you write, named by GUARD_KIT_CONSUMER_RULES_CMD in guard-config.knobs: the shell guard runs it before the generic ruleset, on the harness’s own hook protocol, and --guard-json on the gate binary is its toolkit (guard-kit/SPEC.md §Consumer rules).

  2. Wire the hooks — merge templates/settings-hooks.json into your harness settings file, GUARD_KIT_SETTINGS (the shell-guard on PreToolUse(Bash|PowerShell); the optional wakeup-guard on ScheduleWakeup|CronCreate; and an optional third block showing the path-shaped shape a consumer kit’s own guard registers under — lifecycle-kit’s workflow-state-guard is the shipped instance).

    Then review templates/settings-allow.json against guard-kit/SPEC.md §The recommended allowlist, and union the entries you accept into that file’s permissions.allow.

  3. Gitignore the two scratch logs (.workflow/prompt-friction.log, .workflow/wakeup-attempts.log) and their drain companions (.workflow/*.drain, .workflow/*.drain.part) — both logs are per-iteration, drained at close.

  4. Splice templates/close-triage.md into your close-stage skill (it fills lifecycle-kit’s housekeeping slot, close step 4).

Configuration is a knob file — override any knob in guard-config.knobs, one NAME = value, NAME[] = element or NAME[key] = value line each (gate-sdk/SPEC.md §The knob file). --emit knob-roster on the gate binary GATE_SDK_NATIVE_BIN names prints every default, and guard-kit/SPEC.md §Layout and configuration owns each. Defaults are this repo’s layout; a knob whose contents are your project’s vocabulary defaults to empty. Drop a tool from GUARD_KIT_SEARCH_TOOLS when your harness build does not carry it, so the find and git grep steers never name a tool that is not there. A member you add to GUARD_KIT_RO_BINS also takes a GUARD_KIT_RO_FORMS declaration of its write and execute forms (guard-kit/SPEC.md §The generic ruleset, rule ro_pipeline), or the read-only pipeline grant withholds it.

Use

Run these arms with the gate binary GATE_SDK_NATIVE_BIN names, where init places it. In PowerShell, spell each line ./scripts/checkwright-gates <arm> at the repository root, with .exe on native Windows:

gates="${GATE_SDK_NATIVE_BIN:-./scripts/checkwright-gates}"
"$gates" --emit scan-prompts          # rank what nothing granted, filtered by the allowlist
"$gates" --emit scan-prompts --count  # <patterns>/<occurrences> token (drift KPI)
"$gates" --emit compare-settings-allow  # local-overlay entries a committed glob already grants, those a probe proves too broad, and those naming a script that does not exist
"$gates" --rewrite [--regex] [--expect <n>] [--] <find> <replace> <file>…  # replace text in tracked files, printing every changed span

--emit scan-prompts takes an optional log path, which overrides GUARD_KIT_LOG and composes with --count in either order; -- ends option processing, so a log path spelled with a leading dash is still reachable. An unrecognized --prefixed argument, --help included, is a refusal that prints the arm’s usage at exit 2.

Test

Run this arm the same way:

gates="${GATE_SDK_NATIVE_BIN:-./scripts/checkwright-gates}"
"$gates" --run-guard-tests    # decision-table over the generic ruleset, then your own rules' table

To test your own rules, name a table in cases.tsv’s grammar with GUARD_KIT_CONSUMER_CASES, a firing and a non-firing row per rule; the arm runs its rows against your rule command (guard-kit/SPEC.md §Testing).

License

Licensed under Apache-2.0, with the text in LICENSE beside this file in an installed copy and at the repository root in the source tree.