Authoring Monitors
The monitor file
A monitor lives in one of two forms:
.claude/monitors/<monitor-id>.md # flat form — id = filename without .md .claude/monitors/<monitor-id>/MONITOR.md # folder form — id = folder name
Both forms are equivalent. The folder form (what agentmonitors init scaffolds) lets you keep related assets alongside the monitor. Either way, the identity is the name — a stable machine identifier, not a frontmatter field.
Frontmatter structure
--- name: Human-readable display name # optional; identity comes from the directory/filename watch: # required — what to observe type: <source-type> # discriminated union tag — always explicit # ... source-specific config ... urgency: high # optional — high | normal | low (default: normal) notify: # optional — delivery timing strategy: debounce settle-for: 5m ---
The minimal valid monitor is just a watch: block and a body — everything else (urgency, name, notify, shape, payload, baseline-strategy, tags) is optional and reveals itself only when a specific need calls for it. An omitted urgency defaults to normal.
Want to be notified during a session? Set
urgency: high. With the standard Claude Code plugin,highevents are surfaced at the next turn boundary, while the defaultnormal(andlow) events are held for the next session's recap rather than interrupting the current one (see Urgency). For "tell me when X changes so I can react now",highis the right choice — this is the one field worth setting even though it's optional.
The body: handling instructions
The Markdown body after the frontmatter is delivered verbatim to the agent when the monitor fires. Write it as a prompt fragment that tells the agent what to do with the signal:
--- watch: type: file-fingerprint globs: ['tsconfig.json', 'package.json'] --- Config files changed. Check whether the change affects build output or dependencies. Run `pnpm check` to verify type checking still passes. If package.json dependencies changed, run `pnpm install`.
The runtime carries the body through unchanged — it never acts on it. All judgment is yours, executed by the agent.
Enable and verify delivery
Authoring a monitor only defines the signal. To verify that any source can reach the agent, enable the project and run the same hook commands the Claude Code plugin runs.
Create .claude/agentmonitors.local.md in the project root:
--- enabled: true ---
Then validate and start a session from that same root:
agentmonitors validate .claude/monitors
printf '{"session_id":"verify-1","cwd":"%s","hook_event_name":"SessionStart"}' "$PWD" \
| agentmonitors session start
Make the watched thing change, wait for the source interval and any settle window, then ask the hook for pending delivery:
printf '{"session_id":"verify-1","cwd":"%s","hook_event_name":"UserPromptSubmit"}' "$PWD" \
| agentmonitors hook deliver
This UserPromptSubmit payload verifies the normal turn-interruptible path. If you are verifying a low-urgency monitor, use the idle lifecycle override instead:
printf '{"session_id":"verify-1","cwd":"%s","hook_event_name":"UserPromptSubmit"}' "$PWD" \
| agentmonitors hook deliver --lifecycle turn-idle
hook deliver reads the enabled project from .claude/agentmonitors.local.md; it is not a generic "talk to any hand-started daemon" command. If the project is not enabled in that local file, the hook exits successfully and prints nothing.
Bundled observation sources
file-fingerprint
Watches local files for content changes using SHA-256 hashing over globs.
watch:
type: file-fingerprint
globs:
- 'src/**/*.ts'
- '*.config.js'
ignore:
- 'src/generated/**'
cwd: /path/to/project # optional — defaults to the workspace/config root
Uses a baseline-then-detect pattern: the first observation establishes the baseline; subsequent observations diff against it. A single isolated run cannot detect change.
When cwd is omitted, relative globs match files under the workspace/config root — the project directory containing .claude. A relative cwd resolves against that same root; an absolute cwd is used as-is.
Use ignore for files your fired action writes back into the project. For example, if you watch '**/*.txt' and the action records notified-2026-06-29.txt, add ignore: ['**/notified-*.txt'] so the monitor does not detect its own output and re-fire. Ignored paths are excluded from both the baseline and later change detection.
api-poll
Polls an HTTP endpoint and detects response changes.
After writing an api-poll monitor, use the enable and verify delivery recipe to confirm that endpoint changes reach the agent.
Watching a web page needs no change-detection block — the strategy is inferred from the response Content-Type:
watch: type: api-poll url: 'https://example.com/page' interval: 5m # optional — polling interval (e.g. 5m, 30s, 1h)
The full set of options:
watch:
type: api-poll
url: 'https://api.example.com/status'
method: GET # optional — defaults to GET
interval: 5m # optional — polling interval (e.g. 5m, 30s, 1h)
change-detection: # optional — inferred from Content-Type when omitted:
strategy: json-diff # set ONLY to override · text-diff for HTML/plain · json-diff for JSON · status-code for status-only
auth: # optional
type: bearer
token-env: API_TOKEN # reads from environment variable
headers: # optional
Accept: application/json
Change detection strategies:
| Strategy | Behaviour | Use for |
|---|---|---|
text-diff | Compares raw response body text | HTML pages, plain-text status pages — the right choice for watching a web page |
json-diff | Parses JSON and compares semantically (ignores key order and whitespace) | JSON APIs |
status-code | Only detects changes in HTTP status code | Watching whether an endpoint goes up/down (e.g. 200 → 503) |
change-detection is optional. When you omit it, the strategy is inferred from the response Content-Type: a JSON media type (application/json or any +json suffix) uses json-diff; everything else — HTML, plain text, or a missing/unknown Content-Type — uses text-diff. So the common "tell me when this web page changed" case is zero-config.
For status pages, prefer the machine-readable status endpoint when one exists. Rendered HTML often contains per-request timestamps, CSRF tokens, nonces, or build metadata; raw text-diff sees those as changes and can fire every poll even when the actual service status is unchanged.
watch: type: api-poll url: 'https://status.example.com/api/v2/status.json' interval: 5m
Many Statuspage-backed sites expose /api/v2/status.json, which is designed to be stable until the reported status changes. If a site only exposes rendered HTML, consider pairing the monitor with a notify.strategy: debounce window and expect more noise.
An explicit strategy always wins — it is used verbatim with no inference or override. So an explicit json-diff against an HTML page stays json-diff (and agentmonitors monitor test warns that the body does not parse as JSON, steering you to text-diff); an explicit text-diff against a JSON body stays text-diff.
A non-2xx response (e.g. a 401 from a missing/invalid token, or a 500 error page) is treated as an errored observation for text-diff/json-diff — the monitor does not baseline on the error body, and monitor test / monitor history surface the status — so a broken auth/URL is not silently masked. (status-code is the exception: there the status itself is the watched signal, so a non-2xx is a legitimate change, not an error.)
Auth types:
- Bearer token:
type: bearerwithtoken(inline) ortoken-env(env var name) - Basic auth:
type: basicwithusernameandpasswordfields
command-poll
Runs a local command on an interval and detects when its output changes — the local-process sibling of api-poll. Use it to watch anything a CLI can report: git status, kubectl get, build tooling, a health-check script.
After writing a command-poll monitor, use the enable and verify delivery recipe to confirm that command-output changes reach the agent.
watch:
type: command-poll
command: # argv array, run directly (no shell)
- git
- status
- --porcelain
interval: 5m # optional — polling interval
change-detection: # optional
strategy: text-diff # text-diff (default) | json-diff | exit-code
cwd: /path/to/repo # optional
timeout: 30s # optional — wall-clock limit
command is an argv array, spawned directly with no shell — what you write is exactly what runs, with no word-splitting, globbing, or injection surface. To use a pipeline or other shell operators, spawn a shell explicitly in argv form:
watch:
type: command-poll
command: ['sh', '-c', 'git status -sb | grep ahead'] # the supported pipeline idiom
change-detection:
strategy: json-diff # use json-diff when the command emits JSON, e.g. curl | jq
Use strategy: json-diff when the output is JSON (compares semantically, ignoring key order and whitespace); text-diff (the default) for plain text; exit-code to fire only when the exit code changes.
Watch an upstream branch
To watch remote commits before they have been fetched locally, query the remote directly:
watch:
type: command-poll
command:
- git
- ls-remote
- origin
- refs/heads/main
interval: 5m
change-detection:
strategy: text-diff
git ls-remote origin refs/heads/<branch> asks the remote for the branch tip without mutating local refs. By contrast, git status, git status -sb, and git rev-parse origin/main only inspect local state; they can stay stale until a fetch or pull updates local refs. Use incoming-changes when you want to react after the local commit graph advances through a pull, merge, fast-forward, or local commit.
schedule
Fires on a cron schedule — no change-detection, purely time-driven.
watch: type: schedule cron: '0 9 * * 1-5' # weekdays at 9 AM timezone: America/New_York # optional — defaults to UTC label: Daily standup reminder # optional — custom display title
incoming-changes
Fires when a git pull or merge advances the commit graph and touches the specified paths. Unlike file-fingerprint, this keys off commit provenance — you know the change came from someone else's push, not your own edit.
watch:
type: incoming-changes
paths:
- 'docs/specs/**'
branch: main # optional — defaults to current branch
incoming-changes is not a remote-ahead detector. It observes the local git graph after the branch in this workspace advances. If you need to know that a remote branch has new commits before a fetch or pull, use the command-poll upstream branch recipe.
Urgency
urgency controls how — and how urgently — the runtime surfaces the signal to the agent. It is optional and defaults to normal; set it explicitly when you want to interrupt the current session (high) or stay quietest (low).
| Value | Delivery behaviour (standard Claude Code plugin) |
|---|---|
high | Surfaced during the session, at the next turn boundary, after a 15 s settle window. Choose this for "tell me when X changes so I can react now." |
normal | Held and surfaced in the next session's startup recap rather than interrupting the current turn. Choose this for background changes the agent should know about but needn't act on immediately. |
low | Same as normal but lowest priority — quietest. |
urgency: high # or normal, or low
urgency also accepts an escalation band lo..hi (e.g. normal..high) — the low bound is the default level and the high bound is the ceiling a source's per-observation salience may escalate to (for example, file-fingerprint raises a file deletion to high). A bare level like normal is just the degenerate band normal..normal. Bands are an advanced control; reach for one only when you want a source to be able to raise urgency on its own.
Why this matters for the simplest case. If you want the agent to be notified mid-session the moment a file or command output changes, use
urgency: high.normal/lowchanges are real and durable — queryable any time viaagentmonitors events listand replayed in the next session's recap — but the standard plugin's turn-boundary hook (agentmonitors hook deliveronUserPromptSubmit) injects only settled high-urgency events into the running turn; it emits nothing fornormal/low, so those do not interrupt the current session. (A lightweight "messages pending" reminder fornormalis available on demand viaagentmonitors hook claim, but the standard plugin does not auto-inject it mid-turn.)
Notify strategies
The notify: block controls delivery timing. Omit it for immediate delivery.
Debounce
Wait for a quiet period before notifying. Good for rapid-fire events like file saves:
notify: strategy: debounce settle-for: 5m # wait 5 minutes of quiet before notifying
Throttle
Notify immediately on first event, then suppress for a cooldown:
notify: strategy: throttle suppress-for: 30m # notify once, then ignore for 30 minutes
Rollup
Accumulate everything since the last window and deliver it as a single digest on a schedule — delivery time is the constraint, not change time. Good for a daily summary instead of per-change pings:
notify: strategy: rollup window: '0 9 * * 1-5' # cron — when the digest is delivered (weekdays at 9 AM) timezone: America/Los_Angeles # optional — defaults to UTC
Pair rollup with a relaxed watch.interval (e.g. 1h) — there is no benefit to polling every 30 s when delivery is once a day.
Progressive disclosure
The design rule: simple stays simple, power reveals only when a real need calls for it. Reach for each field in this order, and only when its specific friction shows up:
- Start minimal —
watch:(what to observe) and a body (what to do). This is enough for the great majority of monitors. Addurgency: highhere too if you want to be interrupted mid-session (the defaultnormalwaits for the next session's recap). notify:— when a monitor fires too often. Adddebounceto wait for quiet,throttleto cap frequency, orrollupto batch into a scheduled digest. (See Notify strategies.)shape:— when the agent shouldn't have to recompute facts from raw data. Declare derived facts (e.g.past-due,urgent) so the diff is over meaning, not noise.payload:— when the recipient needs a specific delivery form (structuredJSON for a computing agent,prosefor a human-readable digest) rather than the default text.baseline-strategy:— when an agent rejoining after a gap needs the full play-by-play (incremental) instead of the default net diff (net).
Each step is independent; you never need a later one to use an earlier one. Start at step 1 and stop as soon as the monitor does what you want.
See the use cases page for real patterns across this spectrum.
Validate
agentmonitors validate .claude/monitors
Generate a JSON Schema for editor autocompletion
agentmonitors schema generate -o monitor-schema.json
Point your editor at the generated file to get inline validation and autocomplete while authoring monitors.