Skip to main content

Agent-Surface Sweeps

An agent's reach is not fixed. Skills get installed, and each one widens what the agent can do without anyone deciding that it should. A sweep enumerates the installed skills, records a verdict for everything it examined, and — the part that matters — records everything it could not examine and why.

When this matters

A scan that found nothing and a scan that could not look produce the same report in most tools: an empty findings list. That is the failure this feature exists to remove. A permission error in one skill directory should not be indistinguishable from a clean skill directory.

Sweeps are also how you answer "what changed?" without re-reading everything yourself. Two sweeps you can compare tell you what moved; one sweep only describes today.

What a sweep records

Each sweep writes one record with two lists.

Examined — one entry per item the sweep actually read:

FieldMeaning
kindskill, mcp_config, or plugin — only skill is produced today
agentWhich agent's surface it belongs to
name, pathIdentity and location
content_hashHash of the content, not the path
verdictclean, flagged, or quarantined
rule_idsWhich rules fired, if any

Skipped — one entry per item the sweep could not read:

FieldMeaning
kind, agent, pathWhat could not be reached
reasonA closed enum, never free text
detailHuman-readable specifics

The reason values are permission_denied, not_found, unreadable, parse_error, too_large, symlink_loop, and root_missing. Keeping them a closed set rather than a free-text string is deliberate: coverage gaps can be counted and asserted on instead of grepped for.

note

content_hash keys identity to content rather than to path and mtime, so a modified skill cannot look unchanged by keeping its filename and timestamp.

Each record also carries a triggerinstall_gate, watch, periodic, or manual — so a sweep you ran by hand is distinguishable from one the daemon ran on its own schedule.

Running a sweep

The daemon sweeps periodically on its own, by default every six hours. That loop is off entirely when the skill dir-watcher is disabled via belay skill-watch off or the rescan interval is set to 0.

To sweep immediately in the current process instead of waiting:

belay sweep-now
belay sweep-now --format json

Reading history and drift

belay sweep-history # recent sweeps, newest first
belay sweep-history --limit 5
belay sweep-compare # the two most recent
belay sweep-compare --from <sweep_id> --to <sweep_id>

sweep-compare classifies every item as new, persisting, changed, resolved, or unknown. The unknown class is the honest one: it is where an item lands when a sweep could not reach it, so an item does not silently migrate into "resolved" just because this run failed to open it.

All three commands read the same sweeps.ndjson history, and evidence packs carry sweep history with them.

Honest limits

  • sweep-now is not a substitute for the periodic loop. Alert dedup state is per-process, so running it repeatedly from cron re-alerts items that are still flagged rather than behaving like the daemon's own schedule. Use it on demand; leave the loop on for continuous coverage.
  • Sweeps currently enumerate installed skills only. The record format has mcp_config and plugin item kinds and the comparison logic already handles them, but nothing enumerates them yet, so every row a sweep writes today is a skill. Read a clean sweep as "no flagged skills", not as "nothing anywhere widened this agent's reach".
  • A sweep does not cover your repository. What an agent does inside a repo it is already allowed to touch is handled at the tool-call boundary, not here.
  • Recording a gap does not close it. A permission_denied entry tells you the sweep could not look. Acting on that is still yours to do.