Skip to main content

agent-guard

Turns the required_agents clause in policies/*.yaml from text into a runtime gate. A policy can declare "any change to **/*.controller.ts requires backend-developer and code-reviewer in context" — this skill is what actually checks that the declaration is honored before code is written, instead of discovering after the fact that the wrong (or no) expert shaped the change.

It is agent-agnostic: it never hardcodes agent names. It reads them dynamically from whichever policies match the files at hand, so it keeps working as policies and agents are added. (This is also why it is a skill and not a hook: whether an agent is loaded in the current context is model-side state a bash hook cannot see.)

When this fires

  1. Step 5 of /continue — the loop hands the aggregated required_agents from its policy-evaluation step (step 4) to this skill to load them.
  2. As a self-check before any Edit / Write — when about to modify files, run this to confirm the right experts are in context for what those files trigger. This is the catch for sessions that did not enter through /continue.
  3. On demand — "which agents do I need for this change?", "am I cleared to edit this?".

The procedure

  1. Determine the file set. The files about to be touched (from the plan, the staged diff, or the explicit target of the imminent Edit/Write). If unknown, derive from the task scope.

  2. Locate the policy files, then match. The policies do not live in the consumer repo — they ship inside the plugin. Resolve the policy directory before matching:

    # Default (any repo): read policies from the installed plugin.
    PLUGIN_ROOT="$(jq -r '.plugins["bewith@bewith-dev"][0].installPath // empty' ~/.claude/plugins/installed_plugins.json 2>/dev/null)"
    POLICY_DIR="${PLUGIN_ROOT:+$PLUGIN_ROOT/policies}"
    # Override inside the platform repo (bewith-docs) — test against the working tree, not the cached install.
    [ -f ./.claude-plugin/marketplace.json ] && POLICY_DIR="./policies"
    ls "$POLICY_DIR"/*.yaml

    The triggered_by globs are always matched against the consumer's changed files (the repo you are working in) — only the policy definitions come from the plugin. Glob $POLICY_DIR/*.yaml, so a policy added to the plugin is picked up automatically on the next plugin update; no per-repo wiring. If $POLICY_DIR resolves to nothing (plugin not installed and not in the platform repo), state that policy evaluation was skipped — do not fabricate a policy set.

    For each policy file, test its triggered_by entries (each is a file_change: glob) against the changed-file set. Collect every policy that matches.

  3. Aggregate required_agents. Union the required_agents list across all matched policies. This is the set that MUST be in context. Also note each matched policy's required_gates and any human_approval_required — surface them, but the agent set is this skill's enforcement target.

  4. Check what is loaded. Compare the required set against the agents currently in context (those whose bodies have been read into this session, e.g. via @agent-<name> or a prior load step).

  5. Resolve the gap.

    • All required agents present → pass. State which policies matched and which agents satisfied them, then proceed.
    • One or more missingblock. Emit the block message (below). Load each missing agent (.claude/agents/<name>.md) before any Edit/Write proceeds. Re-run the check after loading.
  6. No policy matched → pass with a note ("no policy triggers for these paths; proceeding without a required-agent constraint"). Absence of a policy is not a failure — most files trigger none.

Block message format

When a required agent is missing, lead with the gap — do not bury it:

agent-guard: BLOCKED — required agent not in context

Files: <the paths that triggered this>
Matched policy: <policy-id> (triggered_by: <the glob that hit>)
requires: <agent-a>, <agent-b>
in context: <agent-a>
MISSING: <agent-b>

Loading .claude/agents/<agent-b>.md before proceeding.
Re-running agent-guard after load.

If multiple policies matched, list each with its own requires/missing breakdown.

What this skill does NOT do

  • It does not evaluate the policy's gates (contract_diff, consumer_scan, etc.) — those run elsewhere in the loop. It only enforces the agent clause.
  • It does not decide policy matching rules — it reads triggered_by as written. If a glob is wrong, fix the policy, not this skill.
  • It does not load the whole roster. Only the agents the matched policies require — lazy loading is the point (see /continue step 5).
  • It does not hardcode any agent name. Every name comes from a matched policy at runtime.

Cross-references

  • /continue — step 5 invokes this skill with the policy-evaluation output from step 4.
  • policy-engine.md — defines required_agents, triggered_by, and the run algorithm this skill operationalizes.
  • policies/*.yaml — the policy set whose required_agents clauses this skill enforces; policies/schema.json is the contract.
  • artifact-types.md — why this is a skill (agent-agnostic procedure) and not a hook (context-state the hook cannot read) or a policy (it enforces policies, it is not one).
  • enforcement-tiers.md — where this self-check sits among the platform's enforcement layers.