Audit Trail & Alerts
How Guard records every policy decision as an audit event, rolls those events up into aggregates for reporting, and the alert-rule schema behind webhook notifications when a condition is met.
Audit Trail & Alerts
Every request Guard evaluates — allow, deny, or monitor — is written to an append-only audit trail. Guard rolls those events up into hourly and daily aggregates for reporting and trend analysis. Guard also defines an alert-rule schema — thresholds on signals like a rising jailbreak score or an elevated ratio of blocked requests that are meant to fire a webhook — though as covered below, rule creation and evaluation aren't wired up yet. This page covers what's recorded, how to query it, and the alert-rule reference.
Every decision is an audit event
Each time Guard evaluates a request, it writes an audit_events row recording the decision and the signals behind it:
| Column | Description |
|---|---|
id, org_id, agent_id, server_id, proxy_url_id | Identifiers for the event and the agent/server/proxy URL it relates to |
request_id, method, tool_name | Which request this was and which tool it targeted |
action | The decision Guard made: allow, deny, or monitor |
rule_id, rule_name, deny_reason | Which policy rule matched, and why, when the action wasn't a plain allow |
risk_score, jailbreak_score, anomaly_score | The scores Guard's analysis produced for this request |
input_params, output_result | The request's input parameters and the resulting output, stored as JSON |
pii_detected, fields_redacted | Any PII types detected and any fields redacted from the payload |
latency_ms, status_code, error_message | Timing and outcome of the underlying call |
created_at | When the event occurred |
Note that action is a three-way decision, not a simple allow/block: monitor covers requests Guard let through but flagged for visibility, distinct from an outright deny.
A denied request looks like this once it lands in the audit trail:
{
"id": "b6e1c2b0-3e4a-4f1b-9c2d-1a2b3c4d5e6f",
"org_id": "3fa0e6d2-...",
"agent_id": "9c1e4a2b-...",
"server_id": "7d2f5b3c-...",
"request_id": "req_01HZX...",
"method": "tools/call",
"tool_name": "run_sql_query",
"action": "deny",
"rule_id": "2f8a...",
"rule_name": "Block raw SQL from untrusted agents",
"deny_reason": "input matched block_pattern rule",
"risk_score": 0.83,
"jailbreak_score": 0.12,
"anomaly_score": 0.91,
"pii_detected": ["email", "credit_card"],
"fields_redacted": ["customer.email", "customer.card_number"],
"latency_ms": 4,
"status_code": 403,
"created_at": "2026-07-07T14:02:31Z"
}input_params and output_result are also present on every event (omitted above for brevity) — they carry the request's parameters and the tool's response as JSON, subject to whatever output filters your policy applies before the response reaches the audit trail.
The audit_events table is partitioned by month on created_at. A background worker task keeps roughly the current month plus three months of future partitions created ahead of time, and a separate daily job deletes events older than the retention window — 90 days by default (AUDIT_RETENTION_DAYS on a self-hosted Guard Proxy deployment) — so old data ages out automatically rather than accumulating indefinitely. This is a high-volume, append-only log designed to hold every decision Guard makes, not just the ones that get blocked.
Audit writes are asynchronous and non-blocking. Guard publishes each event onto a Redis Stream on the request's hot path and returns immediately; a background worker consumes that stream and batches events into Postgres — flushing every 100 events or every 1 second, whichever comes first, by default (
AUDIT_BATCH_SIZE/AUDIT_BATCH_TIMEOUT). A slow or temporarily unavailable audit store does not add latency to, or block, the underlying policy decision — but it also means a just-made request can take up to roughly a second to show up when you query the trail immediately afterward.
Querying the audit trail
Two endpoints back the audit views, both under your Guard deployment's management API:
GET /api/v1/audit
Lists individual audit events for your organization, newest first.
| Query param | Required | Notes |
|---|---|---|
action | No | Exact match against allow, deny, or monitor. Omit to return all three. |
method | No | Exact match against the JSON-RPC method, e.g. tools/call. |
from, to | No | Date range filter on created_at. Either or both may be omitted — an omitted bound simply isn't applied. |
page | No | Zero-indexed page number. Defaults to 0. |
page_size | No | Defaults to 50, capped at 200. |
curl "https://<your-guard-host>/api/v1/audit?action=deny&from=2026-07-01&to=2026-07-07&page=0&page_size=50" \
-H "Authorization: Bearer <your-jwt>"{
"data": [
{ "id": "b6e1c2b0-...", "action": "deny", "tool_name": "run_sql_query", "risk_score": 0.83, "created_at": "2026-07-07T14:02:31Z", "...": "..." }
],
"total": 214,
"page": 0,
"page_size": 50,
"has_more": true
}There's no agent_id or server_id query parameter today, even though both are recorded on every event — filter client-side on the returned events, or narrow by method/action/date range instead, if you need to isolate one agent or server.
GET /api/v1/audit/timeline
Returns event counts bucketed by hour or day, computed live over the raw events in the window you ask for — useful for trend charts without pulling every event.
| Query param | Required | Notes |
|---|---|---|
period | No | hour or day. Defaults to hour if omitted. |
from, to | Yes | Unlike GET /audit, both bounds are required — the underlying query uses them directly and errors if either is missing. |
curl "https://<your-guard-host>/api/v1/audit/timeline?period=day&from=2026-07-01&to=2026-07-07" \
-H "Authorization: Bearer <your-jwt>"[
{
"period_start": "2026-07-06T00:00:00Z",
"total_requests": 4820,
"allowed_requests": 4510,
"denied_requests": 260,
"monitored_requests": 50,
"avg_latency_ms": 38.4,
"avg_risk_score": 0.11,
"pii_detections": 37,
"jailbreak_detections": 3
}
]
GET /audit/timelineis unpaginated and returns one row per bucket in the window — a widefrom/torange withperiod=hourcan return a lot of rows. Preferperiod=dayfor anything wider than a week or two.
Aggregates for reporting and trends
Separately from the live GET /audit/timeline endpoint above, an hourly and a daily background job roll audit_events up into a persisted audit_aggregates table — one row per organization, per period (hour or day), per period_start:
| Column | Description |
|---|---|
total_requests, allowed_requests, denied_requests, monitored_requests | Request counts by outcome for the period |
avg_latency_ms, avg_risk_score | Averages across the period |
pii_detections | Count of requests where at least one PII type was detected |
jailbreak_detections | Count of requests where jailbreak_score exceeded a fixed 0.5 cutoff in the rollup job — this is a different, hard-coded threshold from whatever jailbreak_score value you use in a policy rule or an alert rule |
top_tools, top_agents | Reserved fields for the most-active tools and agents in the period |
(org_id, period, period_start) is unique, so re-running the rollup for a period upserts in place rather than duplicating rows.
top_toolsandtop_agentsare present in the schema but are not populated by the aggregation job today — don't rely on them being non-empty.
audit_aggregatesandGET /audit/timelineare two independent things today. The rollup job writes toaudit_aggregateson a schedule, butGET /audit/timelinedoesn't read from that table at all — it computes the same shape of summary live, directly fromaudit_events, for whatever window you pass in. In practice this means the timeline endpoint is always fresh (no rollup lag) but scans raw events for the requested range, and the persistedaudit_aggregatestable isn't currently exposed through any documented endpoint. If you need the exact rollup numbers rather than a live equivalent, queryGET /audit/timelinewith the same period and window — the values match, aside from thejailbreak_detectionscutoff note above and the always-emptytop_tools/top_agentsfields, which the timeline response doesn't include at all.
Alert rules
This capability isn't reachable through the product yet. The
alert_rulesschema, trigger types, and message strings below exist in Guard's codebase, but as of this writing there's no/api/v1/...endpoint to create, list, or update an alert rule, and no running Guard process (proxy, API, or worker) evaluates live traffic againstalert_rulesrows or dispatches a notification when one matches — the trigger-matching and message-building logic isn't currently called from anywhere in the request path or a background job. Treat the rest of this section as a reference for the rule shape and message format rather than a feature you can configure and rely on today.
An alert rule watches a specific signal and fires when it crosses a threshold within a rolling window:
| Column | Description |
|---|---|
name, description | How the rule identifies itself |
trigger_type | What the rule watches: jailbreak_score, blocked_ratio, server_down, anomaly_score, or rate_limit |
threshold | The value that must be met or exceeded to trigger |
window_seconds | The rolling window the threshold is evaluated over (defaults to 300 seconds) |
severity | low, medium, high, or critical |
is_active | Whether the rule is currently enabled |
last_triggered_at | When the rule last fired |
Each trigger_type maps to a specific comparison, and produces a specific message string when it fires:
trigger_type | Fires when | message sent in the alert |
|---|---|---|
jailbreak_score | A request's jailbreak score meets or exceeds threshold | Jailbreak score 0.91 exceeded threshold 0.80 |
anomaly_score | A request's anomaly score meets or exceeds threshold | Anomaly score 0.87 exceeded threshold 0.75 |
blocked_ratio | The ratio of denied requests meets or exceeds threshold, evaluated over the rule's window — not a raw count of denials from one specific agent | Blocked request ratio 0.62 exceeded threshold 0.50 |
server_down | The connected MCP server is unreachable | MCP server is unreachable |
rate_limit | A request's risk score (not literally a request-per-second rate) meets or exceeds threshold | Alert triggered: <rule name> — unlike the other trigger types, the message doesn't include the risk score or threshold value, so give a descriptive name to a rate_limit rule if you want the alert itself to convey what tripped it |
blocked_ratio is the rule to use for something like "an agent is getting blocked repeatedly" — again, it's an org-wide ratio over the window, not scoped to one agent.
The
rate_limitname is a bit of a misnomer: it doesn't watch requests-per-second directly, it compares againstrisk_score— a composite Guard computes per request as a weighted blend ofjailbreak_score(60%) andanomaly_score(40%), capped at1.0. If you want an alert that's purely about jailbreak or anomaly signal, use thejailbreak_scoreoranomaly_scoretrigger types instead; userate_limitwhen you want to watch the blended score.
There isn't a dedicated "tool poisoning" trigger type, and tool poisoning findings don't appear in the audit trail at all. Tool poisoning is a separate, on-demand static analysis of a registered server's tool definitions (its name and description) — distinct from the per-request
jailbreak_score/anomaly_scoresignals onaudit_events, which are computed from request content, not tool metadata. See Tool-poisoning detection for how that check works.
How alerts are delivered
The intended flow is: when a rule's condition is met, Guard sends the alert to every active webhook configured for your organization that's subscribed to the alert.triggered event (or subscribed to all events with *) — see the note above on evaluation not being wired up yet. The payload is built like this:
{
"event": "alert.triggered",
"data": {
"id": "...",
"org_id": "...",
"rule": { "name": "...", "trigger_type": "blocked_ratio", "severity": "high", "...": "..." },
"severity": "high",
"message": "Blocked request ratio 0.62 exceeded threshold 0.50",
"triggered_at": "2026-07-07T00:00:00Z"
}
}Each delivery is signed the same way as any other Guard webhook — an HMAC-SHA256 signature over a timestamp-prefixed payload, sent as a sha256=<hex> header value — so you can verify it came from Guard before acting on it. See Webhooks for the full delivery, signing, and retry behavior.
Alerts, once evaluation is wired up, are delivered via webhook only — there's no separate email or in-app notification channel described anywhere in the codebase today. If you have no active webhook subscribed to
alert.triggered(or*), a triggered rule would still updatelast_triggered_atbut there'd be nowhere for the notification to go.
FAQ & Troubleshooting
Does every allowed request get logged, or only blocks?
Every request Guard evaluates is logged, regardless of action. Filtering the audit trail to action=deny is a query you apply on top of a complete record, not a separate log.
I don't see any alert rule management in the API — how do I create one?
There isn't a documented way to today. The alert_rules table and the trigger/message logic described above exist in Guard's codebase, but no /api/v1/... route creates, lists, or updates them, and no running Guard process evaluates traffic against them yet — so this section documents the rule shape and message format for when that lands, not a feature you can configure right now.
Can I alert on a single agent's block count instead of an org-wide ratio?
The blocked_ratio trigger type evaluates a ratio over the rule's window, not a per-agent denial count. If you need to watch one agent specifically, scope your monitoring around that agent's activity in the audit trail rather than expecting the alert rule itself to isolate it.
Why don't top_tools / top_agents show up in audit_aggregates?
Those columns exist in the schema for future use but aren't populated by the current aggregation job — use GET /audit with your own filters to identify top tools or agents in the meantime.
How long is audit data retained?
90 days by default (AUDIT_RETENTION_DAYS on a self-hosted deployment). A daily job deletes events older than the cutoff, and the underlying table is partitioned by month so the deletes stay cheap as data ages out.
I just made a request — why isn't it showing up in GET /audit yet?
Audit writes are asynchronous: the event is published to a Redis Stream on the hot path and a background worker batches it into Postgres, flushing every 100 events or every 1 second by default. Query again after a second or two — this isn't a bug, it's the tradeoff that keeps audit logging from adding latency to the underlying request.
Can I filter GET /audit by agent_id or server_id?
Not today. Both are recorded on every event, but the endpoint only exposes action, method, from/to, and pagination as query parameters. Filter client-side on the returned page, or narrow by method/date range if you need to zero in on one agent or server.
GET /audit/timeline returns an error — what am I missing?
Unlike GET /audit, both from and to are required for the timeline endpoint; leaving either one out causes the underlying query to fail rather than defaulting to "everything." Always pass an explicit date range.
Is this the same "audit trail" as the one under Settings → Audit Logs in the portal?
No — Portal Audit Trails logs administrative and workspace activity (who changed a policy, exported an SBOM, invited a user) for compliance purposes. This page covers Guard's separate, high-volume log of individual MCP request decisions (allow/deny/monitor). They're stored differently, queried through different endpoints, and retained on different schedules — don't expect one to show up in the other.
Related
- Policies — define the rules whose allow/deny/monitor decisions populate the audit trail.
- Webhooks — configure delivery, signing, and retries for
alert.triggeredand other events. - Guard SDK — embeds Guard in your MCP server/agent process; also covers the separate tool-poisoning detection check, which doesn't write to the audit trail.
- Guard Proxy Deployment — self-hosted deployment whose
workerservice runs the audit rollup, retention, and batch-writer jobs described on this page. - Servers & Agents — the
agent_idandserver_idinventory that every audit event and alert is scoped to. - Portal Audit Trails — the separate, workspace-activity audit log; not to be confused with the request-level trail on this page.
Guard Policies
Author and manage the policies that decide whether a given MCP tool call is allowed, denied, or monitored by Guard.
Guard Webhooks
Deliver Guard alert events — jailbreak scores, anomaly detection, blocked-request ratios, and unreachable servers — to your own endpoint over a signed HTTP webhook.