Guide
Monitor connected sources
A scan tells you what a repository could do on the day you ran it. A monitor runs that scan again on its own, on a schedule and on every push, and tells you what changed: a new lethal trifecta, a model call that lost its governance, a new tool, a new destructive endpoint.
When to use this
- You connected a repository or an API once and want to hear when it changes, without anyone re-running a scan.
- Your agents ship several times a day and a reviewer cannot read every diff for new tools.
- An MCP server you depend on can change its tools after you reviewed them.
| You want to | Run |
|---|---|
| See what is monitored | agentfox scan monitors list |
| Watch a repository | agentfox scan monitors add github_repo owner/repo |
| Watch an API's OpenAPI document | agentfox scan monitors add hosted_api https://…/openapi.json |
| Run one now | agentfox scan monitors run owner/repo |
| Pause or resume one | agentfox scan monitors pause owner/repo |
| Run scheduled work on a self-hosted box | agentfox admin jobs run-due |
| Rescan on every push | POST /api/integrations/github/webhook |
| Send alerts to Slack | AGENTFOX_SLACK_WEBHOOK_URL |
What is watched, and what counts as a change
You rarely create a monitor yourself. One is created when you connect and scan a GitHub repository, scan a hosted API with its OpenAPI URL, or register an MCP server (from the web app, the API, or agentfox scan mcp).
| Kind | Each run | Default interval |
|---|---|---|
github_repo | Downloads the repository with the stored GitHub token and runs the same static scan as agentfox scan. Nothing in it is imported or run. | 6 hours, and on every push once the webhook is set up |
hosted_api | Fetches the OpenAPI document again (public addresses only, as the first scan did). Never calls an operation. | 6 hours |
mcp_server | A remote Streamable HTTP server: reads its tools/list and checks it for drift and hidden instructions. A stdio server is never started; its pushed listings are watched. | 1 hour |
deployed_agent | Sends the live probe library to a probe target someone opted in, and opens a finding when an attack that was contained gets through. Created by the opt-in; the target is never probed more than once in its interval. | The target's own interval (at least 1 hour) |
The first run of a monitor stores a baseline and raises nothing: the scan that created it already showed you what was there. Every later run is compared with the run before it. Line numbers are ignored, so editing the file above a call is not a change.
| Finding type | Severity | Raised when | Closed when |
|---|---|---|---|
monitor_lethal_trifecta | critical | A group of tools or MCP servers can now read private data, read untrusted content and send data out. | The group no longer has all three. |
monitor_governance_removed | high | A model call that was governed (agentfox.auto(), an AgentFox import) no longer is. | It is governed again, or removed. |
monitor_ungoverned_model_call | high | A new model call or agent entrypoint appears ungoverned. | It is governed, or removed. |
monitor_new_tool, monitor_new_mcp_server | medium (low for a tool with no risky capability) | A tool or MCP server appears. | It is removed. Resolve it yourself once reviewed. |
monitor_api_destructive_endpoint | high | A new DELETE, PUT or PATCH, or a POST whose path or summary says delete, pay, refund, send, transfer, run and the like. | The operation leaves the spec. |
monitor_api_new_endpoint | low | Any other new operation. | The operation leaves the spec. |
schema_drift, tool_poisoning | high, critical | An MCP server's tools changed, or a description carries instructions for the model. These are the same findings agentfox scan mcp raises. | Poisoning: when the description is clean. Drift stays until you review it. |
monitor_failing | medium | Three runs in a row could not read the source (token revoked, repository renamed, spec down). | The next run succeeds. |
One condition is one finding. A second run that sees the same trifecta adds nothing; a trifecta that was fixed and comes back reopens the same finding with its history. A run that fails, or that reads no source it understands, closes nothing and keeps the previous baseline: "could not look" is never reported as "clean".
Worked example
Watch a repository
In the web app, connecting and scanning a repository is enough. From the CLI, against a deployment that holds a GitHub connection:
bash agentfox scan monitors add github_repo acme/support-bot --every 6h agentfox scan monitors run acme/support-botOutput monitoring github_repo acme/support-bot every 6h mon_01m47cj46n6r95tqcq github_repo acme/support-bot: baselineSomeone pushes a change
The push removes
agentfox.auto()fromagent.pyand adds a triage bot with three tools. The next run (scheduled, pushed, or by hand):bash agentfox scan monitors run acme/support-botOutput github_repo acme/support-bot: changed + critical acme/support-bot: new lethal trifecta in support_triage/tools.py + high acme/support-bot: governance removed from a model call in support_triage/agent.py + high acme/support-bot: new ungoverned model call in support_triage/tools.py + medium acme/support-bot: new tool 'read_customer_record' in support_triage/tools.py + medium acme/support-bot: new tool 'fetch_url' in support_triage/tools.py + medium acme/support-bot: new tool 'send_email' in support_triage/tools.pyEach line is a finding in the queue, with the file, the call and the fix in its evidence, and an alert if you set them up.
The change is reverted
Output github_repo acme/support-bot: changed - critical acme/support-bot: new lethal trifecta in support_triage/tools.py (cleared) - high acme/support-bot: governance removed from a model call in support_triage/agent.py (cleared) …Closed findings are resolved by
agentfox.capabilities.monitoring, marked automated on the audit chain, so nobody mistakes them for a person's decision.
In the web app Monitor findings carry subject monitor; filter the queue by type monitor_*.
Run it on a schedule
Monitors are run by the monitors.run job, which the job runner enqueues every 10 minutes and which runs only the monitors whose own interval has passed. So how often anything runs is decided by how often the runner itself is called; calling it more often never runs a monitor early or twice.
Hosted (Vercel)
The Vercel cron calls /api/internal/jobs/run once a day (the Hobby tier limit). The repository's .github/workflows/monitors.yml calls it every 30 minutes. Give it two repository secrets:
| Secret | Value |
|---|---|
AGENTFOX_API_URL | The API's base URL, e.g. https://api.example.com |
AGENTFOX_CRON_SECRET | The deployment's cron secret (its CRON_SECRET or AGENTFOX_CRON_SECRET) |
Without them the workflow prints a line and succeeds, so forks do not fail. Any other scheduler works the same way:
curl -fsS -X POST -H "Authorization: Bearer $AGENTFOX_CRON_SECRET" \
https://api.example.com/api/internal/jobs/runSelf-hosted
Run one pass of the job runner from cron, a systemd timer or a Kubernetes CronJob:
*/15 * * * * cd /srv/agentfox && agentfox admin jobs run-duescheduled 6, ran 6, recovered 0AGENTFOX_MONITOR_BATCH_LIMIT (default 5) bounds how many monitors one pass runs; the rest stay due for the next. Set AGENTFOX_SCHEDULER_ENABLED=false to stop all scheduled work.
Rescan on every push
A push to a monitored repository's default branch can queue a rescan of exactly that commit. In GitHub, open the repository (or organisation) settings, Webhooks, Add webhook:
| Payload URL | https://<api host>/api/integrations/github/webhook |
| Content type | application/json |
| Secret | The deployment's AGENTFOX_GITHUB_WEBHOOK_SECRET, or a secret for your connection (below) |
| Events | Just the push event |
On a shared deployment, ask for a secret of your own, shown once:
curl -fsS -X POST -H "Authorization: Bearer $AGENTFOX_TOKEN" \
https://api.example.com/api/integrations/github/webhook-secret{"secret":"q7…","payload_path":"/api/integrations/github/webhook","content_type":"application/json","events":["push"]}Every delivery is checked against X-Hub-Signature-256 before anything else; an unsigned or wrongly signed one is refused with 401, and with no secret configured anywhere the route refuses everything with 503. GitHub's ping shows up as {"accepted": true, "event": "ping"}. A push to another branch is accepted and skipped; set config.branch on the monitor to watch a different one. Ten pushes in a minute queue one rescan of the latest commit.
Alerts
Monitor findings are findings, so the finding webhook already sends them (signed, at or above AGENTFOX_WEBHOOK_MIN_SEVERITY, default high), including when one is resolved. For people, add Slack:
AGENTFOX_SLACK_WEBHOOK_URL: an incoming-webhook URL for the whole deployment, filtered byAGENTFOX_SLACK_MIN_SEVERITY(default medium).- A tenant's own channel, stored encrypted:
PUT /api/alerts/slackwith{"url": "https://hooks.slack.com/services/…", "min_severity": "high"}. Onlyhooks.slack.comURLs are accepted.POST /api/alerts/slack/testsends a test message.
AgentFox · New · CRITICAL: acme/support-bot: new lethal trifecta in support_triage/tools.py
github_repo acme/support-bot · Open findingThe link points at AGENTFOX_CONSOLE_URL/app/findings/…; without that setting the message carries the finding id instead. Both the webhook and Slack need AGENTFOX_ALLOW_EGRESS=true: an alert carries finding titles out of the deployment, so with egress off nothing is sent. Fetching the repository, the spec or an MCP listing is not gated by it: those are reads you asked for, and carry no data out.
Over HTTP
| Call | What it does |
|---|---|
GET /api/monitors | Every monitor, its last result and next run. |
POST /api/monitors | {"kind", "target", "interval_seconds"?, "config"?}. 409 if already monitored. |
GET /api/monitors/{id} | One monitor and its open findings. |
PATCH /api/monitors/{id} | Name, interval (5 minutes to 30 days), config, enabled. |
POST /api/monitors/{id}/pause, /resume | Stop or restart scheduled runs. Pausing closes nothing. |
POST /api/monitors/{id}/run | Run now through the job queue. |
DELETE /api/monitors/{id} | Stop watching. Findings are kept. |
Reading is open to every role; changing monitors needs owner, admin, security or developer, and Slack settings need owner, admin or security. Full schemas are in the HTTP reference.
Troubleshooting
- Status
failed, "no GitHub account is connected": the tenant has no GitHub connection, or it was removed. Connect again from the web app. - Status
inconclusive: the rescan read no Python, TypeScript or JavaScript. The baseline and findings are kept as they were. - Nothing runs: check that something calls the runner (the workflow's secrets, your crontab), that
AGENTFOX_SCHEDULER_ENABLEDis not false, and the monitor'snext_run_at. - The webhook answers 401: the secret in GitHub differs from the deployment's or the connection's. Rotating the connection secret invalidates the old one.
Limits
- Diffs are as good as the static scan: code generated at runtime is invisible to both runs.
- A stdio MCP server's tools are only seen when someone pushes a listing.
- An MCP server that needs credentials to list its tools cannot be read yet.
- Monitoring reports change. What was already there when the monitor was created is in the scan that created it.