Skip to main content

Coding Agent Protection — Observers & Events

pavri-endpointd (the Pavri endpoint sensor) observes coding-assistant activity on managed developer endpoints and emits structured events to the Pavri gateway. This page describes what the sensor detects, the five event variants it emits, and how to query them in ClickHouse.

Policy enforcement and dashboard surfacing for these events land in CAP-1.5 and CAP-1.6 respectively.


What CAP detects

The sensor uses OS-native primitives to observe three categories of activity:

  • Process spawns — every process exec on the endpoint is captured (macOS: Endpoint Security ES_EVENT_TYPE_NOTIFY_EXEC; Linux: eBPF sched_process_exec tracepoint). The sensor walks the process ancestry to attribute the exec to a known coding assistant by binary signature, code-signing identity, and install path.
  • File reads and writes near sensitive paths — file I/O on configured sensitive paths (credential stores, workspace instruction files, /etc, git directories, etc.) is captured (macOS: ES ES_EVENT_TYPE_NOTIFY_OPEN/WRITE; Linux: eBPF LSM hooks on security_file_open / vfs_write). Events are attributed to the assistant that owns the process chain.
  • Assistant discovery events — when the sensor first encounters a known assistant binary (by signature match), it emits a one-time discovery event recording the install location, code-signing identity, and workspace context. This keeps the Pavri assistant inventory current without requiring a separate scanner.

Event variants emitted to the gateway

All events travel to the gateway as EventEnvelope protobuf messages (reusing the existing Pavri telemetry envelope). Five new payload variants are introduced in CAP-1.2:

VariantProto fieldDescription
CodingActionEventEventEnvelope.coding_actionA process exec, file read/write, shell command, git operation, MCP tool call, or other assistant-attributed action. The kind field distinguishes action types (shell_exec, file_read, file_write, net_connect, git, mcp_tool_call, code_handoff, clipboard).
AssistantDiscoveryEventEventEnvelope.assistant_discoverySensor detected a new assistant install on this endpoint. Emitted once per (kind, binary_path, code_signing_authority) tuple. Used to populate and refresh the assistant inventory in Pavri.
CodingSessionEventEventEnvelope.coding_sessionSession lifecycle event (started, closed, suspended, resumed). For CAP-1.2 the sensor emits session events; dashboard surfacing of coding sessions lands in CAP-1.6.
ConfigDriftEventEventEnvelope.config_driftWorkspace instruction source changed (CLAUDE.md, .cursorrules, .windsurfrules, etc. modified since last scan). Used by the detection cascade to flag instruction-poisoning candidates. Full detection integration lands in CAP-1.4.
EndpointPostureEventEventEnvelope.endpoint_posturePeriodic posture snapshot from the sensor: current observer health, drop counters, cert expiry. Supplements the heartbeat and does not insert a row in coding_events.

ClickHouse query examples

Events land in the coding_events table (MergeTree, partitioned by month, 90-day TTL). AssistantDiscoveryEvent and EndpointPostureEvent do not insert coding_events rows; they are available via NATS JetStream subjects only.

Note: The assistant_kind and workspace_id columns on CodingActionEvent rows are empty in CAP-1.2. They are populated in CAP-1.5 when the gateway gains access to the session record for full attribution enrichment.

-- Top assistants seen in the last hour (by session event rows)
SELECT assistant_kind, count() AS event_count
FROM coding_events
WHERE at > now() - INTERVAL 1 HOUR
AND assistant_kind != ''
GROUP BY assistant_kind
ORDER BY event_count DESC;

-- File-write events to sensitive paths in the last 24 hours
SELECT
endpoint_id,
session_id,
path_or_target,
verdict,
at
FROM coding_events
WHERE kind = 'file_write'
AND at > now() - INTERVAL 24 HOUR
AND (
path_or_target LIKE '/etc/%'
OR path_or_target LIKE '%/.ssh/%'
OR path_or_target LIKE '%/.aws/%'
)
ORDER BY at DESC
LIMIT 100;

-- Shell exec events denied by policy in the last 7 days
SELECT
endpoint_id,
path_or_target,
verdict_reason,
at
FROM coding_events
WHERE kind = 'shell_exec'
AND verdict = 'denied'
AND at > now() - INTERVAL 7 DAY
ORDER BY at DESC;

-- Event counts by kind for a specific session
SELECT kind, count() AS n
FROM coding_events
WHERE session_id = 'your-session-id'
GROUP BY kind
ORDER BY n DESC;

Column reference for coding_events:

ColumnTypeNotes
org_idLowCardinality(String)Tenant identifier.
endpoint_idStringSensor's stable endpoint identifier.
session_idStringCoding session UUID; empty for non-session rows.
action_idStringPer-action UUID; empty for session lifecycle rows.
kindLowCardinality(String)Action type: shell_exec, file_read, file_write, git, mcp_tool_call, session.started, session.closed, config_drift, etc.
assistant_kindLowCardinality(String)Assistant identifier (e.g., claude_code, cursor). Empty in CAP-1.2 for action rows; populated in CAP-1.5.
workspace_idStringWorkspace root path. Empty in CAP-1.2 for action rows; populated in CAP-1.5.
process_pidUInt32PID of the observed process.
process_binary_hashStringSHA256 of the observed binary.
path_or_targetStringFile path, shell command, git remote, or MCP server identifier.
byte_countUInt64Bytes read or written (file events only).
verdictLowCardinality(String)Policy enforcement decision: allow, denied, approved, warn. Empty until policy enforcement lands in CAP-1.5.
verdict_reasonStringHuman-readable reason for the policy decision.
human_in_loopUInt8 (bool)Whether a human approval was recorded for this action.
detection_idsArray(LowCardinality(String))Detection IDs from the T1/T2/T3 cascade (populated in CAP-1.4+).
atDateTime64(3, 'UTC')Event timestamp (millisecond precision).
labelsMap(LowCardinality(String), String)Arbitrary key-value metadata attached by the sensor or gateway.

Full dashboard surfacing of coding events lands in CAP-1.6.