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
- Step 5 of
/continue— the loop hands the aggregatedrequired_agentsfrom its policy-evaluation step (step 4) to this skill to load them. - 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. - On demand — "which agents do I need for this change?", "am I cleared to edit this?".
The procedure
-
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.
-
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"/*.yamlThe
triggered_byglobs 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_DIRresolves 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_byentries (each is afile_change:glob) against the changed-file set. Collect every policy that matches. -
Aggregate
required_agents. Union therequired_agentslist across all matched policies. This is the set that MUST be in context. Also note each matched policy'srequired_gatesand anyhuman_approval_required— surface them, but the agent set is this skill's enforcement target. -
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). -
Resolve the gap.
- All required agents present → pass. State which policies matched and which agents satisfied them, then proceed.
- One or more missing → block. Emit the block message (below). Load each missing agent (
.claude/agents/<name>.md) before any Edit/Write proceeds. Re-run the check after loading.
-
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_byas 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
/continuestep 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 whoserequired_agentsclauses this skill enforces;policies/schema.jsonis 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.