Agentic Detection & Response (ADR) Sensor - Security observability for AI coding agents.
ADR Sensor is a Python library that collects telemetry from AI coding agents to enable security monitoring, threat detection, and observability. It parses logs from multiple AI agent platforms and normalizes them into a unified schema for downstream analysis.
Paper: ADR: An Agentic Detection System for Enterprise Agentic AI Security
Code: github.com/uber/ADR
| Agent | Source key | Log Format | Platform |
|---|---|---|---|
| Claude Code | claude |
JSONL (~/.claude/projects/) |
macOS, Linux, Windows |
| Cursor IDE | cursor |
SQLite (state.vscdb) |
macOS, Linux, Windows |
| Cline (Claude Dev) | cline |
JSON task files | macOS, Linux, Windows |
| Claude Desktop | claude_desktop |
JSONL audit logs | macOS, Windows |
| OpenAI Codex CLI | codex |
JSONL + SQLite path catalogs | macOS, Linux, Windows |
| GitHub Copilot CLI | copilot |
JSONL (~/.copilot/session-state/) |
macOS, Linux, Windows |
| DeepSeek Harness | dsh |
JSONL/Zstandard (~/.dsh/sessions/) |
macOS, Linux, Windows |
| Warp Terminal | warp |
SQLite (warp.sqlite) |
macOS, Windows |
| opencode | opencode |
SQLite (opencode.db) or JSON tree |
macOS, Linux |
| Gemini CLI | gemini |
JSONL journals + legacy JSON chats | macOS, Linux, Windows |
| Google Antigravity | antigravity |
JSONL (brain/<id>/.system_generated/logs/transcript.jsonl) |
macOS, Linux, Windows |
The claude source reads transcripts recursively under ~/.claude/projects/,
including subagent transcripts
under <project>/<sessionId>/subagents/agent-<agentId>.jsonl and nested workflow
directories. Main sessions keep the claude_<sessionId> identity; subagents use
claude_<sessionId>_agent_<agentId> and include parent_session_id and agent_id
in session_context so their exports do not overwrite the parent conversation.
String and text-block messages are retained, including user text accompanying tool results. Results are matched by tool-call ID. Malformed records are skipped without discarding surrounding messages. Complete JSON objects concatenated on one physical line and NUL padding between objects are accepted; incomplete tails are skipped without joining physical lines or repairing text inside a message.
Each event includes raw_log_path, a stable conversation-start timestamp, and
session_context.last_event_at and event_count for incremental updates. A file
without any valid timestamp uses its modification time. --save-sessions updates
the saved snapshot when a tool completes or a conversation resumes, even within
the same timestamp second. All recorded branches are retained in file order.
The default lookback is 14 days by file modification time. Existing limits still apply: top-level tool argument strings and tool results are truncated at 1,000 characters; non-text content blocks and separately spilled tool-output files are not imported. Contract tests use synthetic transcripts and do not launch Claude.
The claude_desktop source covers Claude Desktop's local agent mode (released as
Claude Cowork), on both macOS and Windows. Two kinds of session are captured:
- Interactive sessions —
.../local-agent-mode-sessions/<user>/<org>/local_<uuid>/audit.jsonl - Dispatch sessions (delegated background agents) —
.../<user>/<org>/agent/local_ditto_<uuid>/audit.jsonl
Both emit source: "claude_desktop". Dispatch sessions get a distinct
claude_desktop_dispatch_ session-id prefix and an is_dispatch: true flag in
session_context, so detection rules can treat unattended runs differently from
interactive ones. Interactive session ids are unchanged.
The Codex parser reads JSONL rollout files from $CODEX_HOME/sessions/. It also
opens every $CODEX_HOME/state_*.sqlite catalog in read-only mode and supplements
filesystem discovery with rollout paths from compatible threads tables. A table
must have id and rollout_path columns; updated_at and updated_at_ms are
optional. Relative rollout paths are resolved from CODEX_HOME, and only existing
regular .jsonl files are accepted. Catalog and filesystem paths are deduplicated.
If CODEX_HOME is unset or empty, it defaults to ~/.codex. Rollouts use a 14-day
lookback by default. Filtering uses the newer of the file modification time and any
valid catalog update timestamp, so a recently updated catalog entry can retain an
older file. Corrupt, locked, or incompatible catalogs are skipped without affecting
files found under sessions/; malformed timestamps fall back to file modification
time. Pass max_age_days to CodexParser or AgentObserver to customize the
lookback; values less than or equal to zero disable age filtering for CodexParser.
The copilot source reads each GitHub Copilot CLI session's events.jsonl and
optional workspace.yaml and vscode.metadata.json files. Copilot CLI uses the
same home-relative configuration directory on every supported operating system;
it does not use Library/Application Support or AppData for session history:
| Operating system | Default session directory |
|---|---|
| macOS | /Users/<user>/.copilot/session-state/ |
| Linux | /home/<user>/.copilot/session-state/ |
| Windows | %USERPROFILE%\.copilot\session-state\ (typically C:\Users\<user>\.copilot\session-state\) |
Set COPILOT_HOME for both Copilot CLI and ADR Sensor when the CLI configuration
directory has been moved; the Sensor then reads $COPILOT_HOME/session-state/.
Copilot CLI's legacy --config-dir option is deprecated in favor of this
environment variable. These locations and the override are defined by the
Copilot CLI configuration directory reference.
The parser applies a 14-day lookback using each events.jsonl modification time.
Use --all-history to include older sessions. This source covers GitHub Copilot
CLI session state only; it does not read the separate storage used by the VS Code
Copilot Chat extension.
The cline source reads task conversation histories from the Cline extension
(saoudrizwan.claude-dev) across supported VS Code-family editors, including
Visual Studio Code (primary), Cursor, VS Code Insiders, VSCodium, and Windsurf:
| Host | Default task directories |
|---|---|
| macOS | ~/Library/Application Support/<Editor>/User/globalStorage/saoudrizwan.claude-dev/tasks/ |
| Linux | ~/.config/<Editor>/User/globalStorage/saoudrizwan.claude-dev/tasks/ |
| Windows | %APPDATA%\<Editor>\User\globalStorage\saoudrizwan.claude-dev\tasks\ |
The parser scans all existing candidate directories, normalizes user prompts and
assistant tool usages (including <use_mcp_tool> invocations) into the ADR
schema, and deduplicates sessions across editors. Lookback defaults to 14 days by
task file modification time; pass max_age_days to adjust.
An inaccessible candidate is reported in diagnostics and the sensor log without
preventing collection from other editors. Python callers can restrict collection
to one task directory with ClineParser(base_path=...).
opencode uses the XDG layout on every platform,
so its data directory is ~/.local/share/opencode on both Linux and macOS
($XDG_DATA_HOME and $OPENCODE_DB are honored when set). Both storage backends
are read:
- SQLite (current releases) —
opencode.db, oropencode-<channel>.dbon non-stable channels. Opened read-only so a running opencode process is never disturbed. A database is used only if it contains thesession,messageandparttables; otherwise the other candidate databases and then the JSON tree are tried, and a database without those tables is reported asunsupported_schema. - JSON file tree (older releases) — a
storage/directory of per-session, per-message and per-part JSON files, in both the project-scoped and legacy layouts.
MCP tools are namespaced by opencode as <server>_<tool>, so any tool that is not a
known built-in and contains an underscore is recorded as tool_type: "mcp_tool" with
its server_name populated.
The dsh source reads current DeepSeek Harness v3 session logs from
$DSH_HOME/sessions/, or ~/.dsh/sessions/ when DSH_HOME is unset. DSH uses
Zstandard-compressed logs by default; uncompressed JSONL v3 logs are also
supported. When migrations leave several generations in one session directory,
the parser follows DSH and considers only the highest generation. A session is
ingested only when that generation is v3, so historical or future formats are
not silently interpreted with the wrong schema.
Tool arguments and results, PTC sub-dispatches, failure status, approvals,
provider/model context, sandbox mode, permission preset, typed message content,
and recorded token usage are normalized into the Sensor schema. Structured tool
result content, metadata, and compaction replacement provenance are retained in
session_context. Tool names do not carry a universal MCP server identity, so
the parser does not guess one.
The standard 14-day file lookback applies; use --all-history to include older
sessions. Storage and event behavior were
checked against DSH's pinned
JSONL persistence contract
and v3 event declarations.
adr-sensor --source dshThe gemini source reads current chats/**/*.jsonl journals and legacy
chats/*.json conversation snapshots. It captures user/assistant text, tool
arguments and results, recorded status and approval requests, model and token
usage, and nested subagent sessions. Start timestamps remain stable when sessions
resume; --save-sessions refreshes changed snapshots in place.
| Host | Default scan root (per-user home) |
|---|---|
| macOS / Linux | ~/.gemini/tmp/ |
| Windows | %USERPROFILE%\.gemini\tmp\ |
| macOS Seatbelt sandbox | ~/.cache/.gemini/tmp/ (also scanned on macOS) |
GEMINI_CLI_HOME overrides the parent home directory, so the path becomes
$GEMINI_CLI_HOME/.gemini/tmp, not $GEMINI_CLI_HOME/tmp. Project directories
can be hashes or readable identifiers. Project paths come from .project_root
or projects.json; a project hash alone is not a filesystem path. A custom
acquired root can be supplied through GeminiParser(base_path=Path("/capture/tmp")).
WSL/container sessions belong to their own filesystem and home.
ADR consolidates repeated journal messages by ID, so tool progress updates do
not duplicate messages or token totals. Earlier activity survives rewind and
checkpoint records. session_context.history_scope is all_recorded_branches;
this is recorded activity, not a reconstruction of only the model's current
context. Source message metadata, typed content, tool IDs, and recorded thought
summaries remain in session_context. The parser adds no redaction or truncation;
upstream output limits and deleted files cannot be recovered. Unknown or malformed
records do not abort other sessions, and malformed-record counts are reported.
Only CLI chat records are covered. Prompt-only logs.json, editor chat storage,
shell history, unsaved sessions, and files outside these roots are not collected.
The default lookback is 14 days by file modification time; --all-history
includes older files. Explicit mcp_/qualified tool names identify MCP calls;
server attribution is left empty when the recorded name is ambiguous.
uv run adr-sensor --source gemini --no-save
uv run adr-sensor --source gemini --save-sessions --all-historyContracts verified against upstream record types, journal writer, storage paths, and supported platforms. Tests use synthetic records matching these contracts and run on all three hosts; they do not require a Gemini account.
The antigravity source reads conversation transcript journals from Google Antigravity (agy) CLI and agent harness brain storage.
| Product | Default brain directory |
|---|---|
| Antigravity CLI | ~/.gemini/antigravity-cli/brain/ |
| Antigravity 2.0 | ~/.gemini/antigravity2/brain/ |
| Antigravity IDE | ~/.gemini/antigravity-ide/brain/ |
These paths follow the user's home directory on macOS, Linux, and Windows
(%USERPROFILE%\.gemini\... on Windows); see Google's
transcript locations.
ADR also checks the legacy ~/.antigravity/brain/, %APPDATA%\Google\Antigravity\brain\,
~/Library/Application Support/Google/Antigravity/brain/, and
~/.config/google/antigravity/brain/ candidates.
ADR honors ANTIGRAVITY_HOME and AGY_HOME as storage overrides: point either at
the brain directory or its parent. Each conversation has its own directory with
.system_generated/logs/transcript.jsonl and, when available, transcript_full.jsonl.
- Transcript selection: ADR compares usable snapshots and selects the newest recorded revision. For the same revision it prefers untruncated data and the full transcript, regardless of file-copy order. An empty, malformed, or unreadable file does not prevent capture from a usable sibling. Root-level transcript files are also supported.
- Step Types: The parser ingests
USER_INPUT(user prompts),PLANNER_RESPONSE(assistant output,thinkingtext, and tool calls), and model-authoredGENERICoutput. Thinking text is retained with a[Thinking]label. Session times use the records'created_atortimestampvalues, falling back to file modification time. - Tool Normalization: Invoked tools are extracted and mapped to
ToolUsage. Native tools are recorded directly, while MCP server calls (such asmcp_<server>_<tool>or<server>:<tool>) are parsed to populateserver_nameandtool_namewithtool_type: "mcp_tool". - Data preservation: ADR does not clip tool arguments or stringify nested values. If the selected source transcript reports
truncated_fields, the event is markedis_truncated; ADR cannot recover content that is absent from that source. - Lookback & recovery: Default lookback is 14 days by file modification time; pass
max_age_daysor--all-historyto adjust. Malformed lines and invalid timestamps are diagnosed without dropping valid conversation records.
uv run adr-sensor --source antigravity --no-save
uv run adr-sensor --source antigravity --save-sessions --all-history┌─────────────────────────────────────────────────────────────────┐
│ AI Agent Logs │
│ Claude, Cursor, Cline, Codex, Copilot CLI, Warp │
│ Claude Desktop, opencode, Gemini CLI, DeepSeek Harness │
│ Google Antigravity │
└───────────────────────────────┬─────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ Source-Specific Parsers │
│ (Each implements BaseParser) │
└─────────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Unified Schema (AgentEvent) │
│ session_id │ timestamp │ chat_history │ tools │ model │
└─────────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ AgentObserver (Orchestrator) │
│ Ingest → Filter → Display → Export │
└─────────────────────────────┬───────────────────────────────────┘
│
┌────┴────┐
▼ ▼
JSON/JSONL OTLP Logs
Files │
▼
Collector / SIEM
Tagged releases are installed from PyPI:
pip install adr-sensorInstall the optional OpenTelemetry dependencies when OTLP log export is needed:
pip install "adr-sensor[otel]"Or install from source:
git clone https://github.com/uber/ADR
cd ADR/Sensor
pip install .# Ingest from all supported agents
adr-sensor
# Ingest from a specific source
adr-sensor --source claude
adr-sensor --source cursor
adr-sensor --source codex
adr-sensor --source copilot
adr-sensor --source dsh
adr-sensor --source claude_desktop
adr-sensor --source opencode
adr-sensor --source gemini
# Save individual session files (incremental)
adr-sensor --save-sessions
# Export as JSONL
adr-sensor --output-format jsonl
# Include all history (not just last 2 weeks)
adr-sensor --all-history
# Custom output directory
adr-sensor --output-dir ./my-output
# Export the same records to an OTLP/HTTP logs endpoint
adr-sensor --otel-config ./opentelemetry-config.json
# Export to OTLP without also writing JSON files
adr-sensor --no-save --otel-config ./opentelemetry-config.json
# Print only warnings and errors (or pick a level: debug, info, warning, error)
adr-sensor --quiet
adr-sensor --log-level debugSources whose agent only runs on some operating systems are skipped automatically
on other platforms — --source all on Linux will not attempt claude_desktop, for example.
Progress messages go to stdout and warnings and errors to stderr. --log-level
sets the minimum level shown (default info) and -q/--quiet equals
--log-level warning. The ingestion summary table is always printed. Library
callers can use adr_sensor.sensor_log.set_console_level() for the same effect.
from adr_sensor import AgentObserver
# Create observer
observer = AgentObserver()
# Ingest from all sources
events, configs = observer.ingest_all()
# Or from a specific source
events, configs = observer.ingest_all(source_filter="claude")
# Display summary
observer.display_summary(events, configs)
# Save to file
observer.save_to_file(events, configs, output_format="json")
# Analyze events
for event in events:
print(f"Source: {event.source}, Session: {event.session_id}")
print(f"Messages: {len(event.chat_history)}")
for msg in event.chat_history:
if msg.tools:
for tool in msg.tools:
print(f" Tool: {tool.tool_name} ({tool.tool_type})")
print(f" Args: {tool.arguments}")OpenTelemetry export is disabled by default. The Sensor only initializes an
OTLP exporter when --otel-config points to a JSON configuration file. Without
that argument, CLI and file-export behavior are unchanged and no OpenTelemetry
logs are sent.
Start from examples/opentelemetry-config.json:
{
"endpoint": "http://localhost:4318/v1/logs",
"service_name": "adr-sensor",
"headers": {},
"timeout_seconds": 10,
"flush_timeout_seconds": 30
}endpoint must be the complete OTLP/HTTP logs URL, including /v1/logs when
required by the receiver. headers can contain authentication headers. An
optional certificate_file names a PEM certificate bundle; relative paths are
resolved from the configuration file's directory.
Each AgentEvent is sent as an adr.agent.session OpenTelemetry LogRecord. Its
body is the complete dictionary returned by AgentEvent.get_non_null_fields(),
the same content written to JSON/JSONL today. The OpenTelemetry exporter applies
no redaction or field projection, so prompts, responses, tool arguments, tool
results, usernames, hostnames, and local paths can be transmitted. Any
normalization already performed by a source parser still applies.
System-configuration records are sent as adr.system.configuration logs on each
run. Sensor health logs are also sent on every run, even when all session snapshots
are already acknowledged. With --save-sessions, successful session delivery is tracked independently
of local session files. A failed export is retried on the next run, even when the
local JSON already exists. A session is skipped only when its complete normalized
payload was successfully exported to the same destination configuration. Changes
to tool results, destination settings, or configured authentication headers cause
a resend. The checkpoint also accounts for effective OTLP environment headers and
mTLS client certificate/key paths. It does not read credential files: after
changing certificate or key contents in place, remove the destination's checkpoint
to resend sessions. Dynamic HTTP credential-provider plugins
(OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER and its LOGS variant)
are unsupported and cause an explicit error; use configured headers or mTLS.
Delivery checkpoints are hidden .adr-otel-delivery.<hash>.json files in the
session output directory. They contain only hashes, including a destination hash
that accounts for authentication headers; they do not store raw URLs, credentials,
session identifiers, or payloads. Missing, unreadable, or corrupt checkpoints cause
sessions to be retried. The checkpoint is replaced atomically only after flush and
shutdown succeed; a checkpoint write failure exits with an error. --no-save
disables checkpoint reads and writes. Without --save-sessions, every run exports
all collected sessions.
The one-shot Sensor process drains bounded batches and reconciles submitted and
successfully exported counts before reporting success, so a full SDK queue cannot
silently drop records. HTTP success is also checked for an OTLP acknowledgement:
partial rejection or a malformed response fails delivery and leaves the affected
run unacknowledged. Resolve persistent collector rejection before rerunning: OTLP
does not identify individual rejected records, so retrying can resend accepted
records too. Export or checkpoint failures exit with a nonzero status.
Delivery is at least once: a collector may receive data before a timeout, process
interruption, or checkpoint write failure, so retries can duplicate records.
Checkpointing only covers sessions that are collected again on a later run; it is
not a persistent payload queue. Consumers can use adr.event.uuid and a full
payload digest to identify repeated snapshots, since a session UUID alone does not
necessarily change when tool results change.
Use an OpenTelemetry Collector when vendor-specific routing, transformation, retry, or persistent queuing is needed.
Every ingestion run writes a content-free summary for each attempted source,
including runs that produce no sessions. diagnostics.jsonl contains all summaries;
error.log contains only partial and failed summaries. Both live under
--output-dir (default ./output), even when --save-sessions uses its separate
default cache directory or --no-save suppresses captured session files. Each log
rotates at 1 MiB with two backups. Use one active sensor process per output directory
to avoid concurrent rotation races. Diagnostic write failures produce a fixed stderr
warning and do not discard captured sessions.
The versioned adr.sensor.health schema contains timestamp, sensor version, source,
stage, status, fixed reason codes, and aggregate counts. For example:
{"schema_version":1,"event":"adr.sensor.health","timestamp":"2026-01-01T00:00:00.000+00:00","sensor_version":"0.0.0","source":"claude","stage":"parse","status":"partial","suspected_schema_drift":false,"counts":{"events_returned":2,"events_emitted":2,"events_filtered":0},"reasons":{"record_decode_error":1}}Statuses distinguish successful capture (ok), no meaningful output (empty),
absent input (no_input), usable output with observed errors (partial), and
errors without usable output (failed). Age filtering and an incomplete live
tail are expected skips, not errors. suspected_schema_drift is a triage hint for
explicitly unsupported record/content/schema shapes, not proof of an upstream
format change. Generic corruption is reported separately.
All ten parsers report observed recovery failures, but coverage is not exhaustive: some optional metadata/timestamp fallbacks, unknown record kinds, and compressed DSH tail recovery are not classified. Counts describe observed recovery operations, not necessarily unique damaged records. A healthy summary does not prove complete capture; a missing summary also cannot distinguish an idle endpoint from a sensor that never ran. Schedule runs and monitor last-seen health externally.
With --otel-config, health is also sent as OTLP logs (adr.event.type=sensor_health),
including when there are no session records. Health errors use WARN severity;
expected skips use INFO. No OTLP exporter is created without that argument. A failed
export is recorded locally because a broken destination cannot receive its own alert.
--fail-on-error exits nonzero after preserving available capture when an observed
parse/save/diagnostic failure occurs; by default these partial failures are reported
without changing the existing continue-on-error behavior. OTLP failures remain nonzero.
When --resource is enabled, resource.log also marks partial runs unsuccessful.
resource.log rotates at 1 MiB with two backups (resource.log.1, resource.log.2).
New structured diagnostics never include prompts, tool arguments/results, paths,
session IDs, exception messages, or tracebacks. This is a separate operational
schema, not redaction of captured telemetry. Legacy console previews/errors and
older entries already present in error.log are not sanitized by this change.
--log-file also writes the run's log messages as JSON lines next to the
diagnostics: sensor_runtime_errors.jsonl holds warnings and errors, and
sensor_runtime_debug.jsonl holds debug and info messages whatever the console
--log-level is. Each file rotates at 1 MiB with two backups. Runtime logging is
off by default. If the files cannot be opened, the sensor prints one warning and
keeps printing warnings and errors to stderr.
Each record has timestamp, level, component, function, phase,
sensor_version, host_os, exception_type, message and, for errors with a
traceback, stack:
{"timestamp":"2026-01-01T12:00:00.000+00:00","level":"WARNING","component":"parsers.cline_parser","function":"parse_all","phase":null,"sensor_version":"0.1.0","host_os":"Linux","exception_type":"KeyError","message":"[CLINE] Error parsing task /home/me/.cline/tasks/123: 'ts'"}Privacy: unlike the diagnostics above, message and stack can contain local
file paths and error text. Log call sites do not log prompts or tool content, but
treat these files like other local logs. --log-file-content-free drops message
and stack and keeps the remaining fields. --log-identity adds username and
hostname to each record; it is opt-in because it identifies the machine and user.
Each parsed session produces an AgentEvent with the following structure:
{
"uuid": "sha256-hash",
"timestamp": "2025-06-15T10:30:00+00:00",
"source": "claude",
"session_id": "claude_abc123",
"hostname": "my-laptop",
"username": "developer",
"host_os": "Darwin",
"model": "claude-sonnet-4-20250514",
"project_path": "/home/user/my-project",
"chat_history": [
{
"role": "user",
"content": "Help me fix this bug",
"tools": [],
"sequence_id": "msg_0"
},
{
"role": "assistant",
"content": "Let me look at the code.",
"tools": [
{
"tool_name": "read_file",
"tool_type": "tool_use",
"arguments": {"path": "main.py"},
"result": "def hello(): ...",
"status": "success"
}
],
"sequence_id": "msg_1"
}
]
}username is the operating-system account the sensor process runs as (a sensor
running as root reports root); it is never read from environment variables such
as USER. hostname is the machine's host name. host_os is the capturing
machine's platform.system() value (Darwin, Linux, Windows, ...), the same
values resource.log uses. It is not part of the uuid, and OTLP exports also set
the os.type resource attribute. All three are resolved once per run and shared by
session events, system configuration, OTLP exports and runtime logs.
Tool arguments and results can contain source code, credentials, or other sensitive content copied from the local environment. Treat sensor output as sensitive data and review it before sharing.
Parsers that can recover session-level configuration attach it under
session_context. This is the agent's own view of what it was allowed to do, which
is often more useful for detection than the conversation itself. Claude Desktop
agent mode populates the richest version:
{
"session_context": {
"title": "Config review",
"is_dispatch": true,
"session_type": "dispatch",
"cli_session_id": "cli-99",
"memory_enabled": true,
"skills_enabled": false,
"plugins_enabled": true,
"available_slash_commands": ["review", "deploy"],
"init": {
"tools": ["Bash", "Read"],
"mcp_servers": [{"name": "github"}],
"permission_mode": "acceptEdits",
"model": "claude-sonnet-4",
"claude_code_version": "2.1.0",
"plugins": ["reviewer"],
"skills": ["pdf"]
}
}
}Parsers may attach normalized per-turn and cumulative token counters to an
AgentEvent. Only counters reported as nonnegative integers are included:
{
"token_usage": {
"last_turn": {
"input_tokens": 120,
"cached_input_tokens": 80,
"output_tokens": 30,
"reasoning_output_tokens": 10,
"total_tokens": 150
},
"cumulative": {
"input_tokens": 420,
"cached_input_tokens": 200,
"cache_write_input_tokens": 40,
"output_tokens": 90,
"reasoning_output_tokens": 25,
"total_tokens": 510
},
"model_context_window": 128000
}
}ADR Sensor is designed to be extensible. To add support for a new AI agent:
- Create a new parser in
adr_sensor/parsers/:
from pathlib import Path
from adr_sensor.parsers.base_parser import BaseParser
from adr_sensor.schemas.agent_event_schema import AgentEvent, ChatMessage, ToolUsage
class MyAgentParser(BaseParser):
def __init__(self, max_age_days: int = 14):
self.base_path = Path.home() / ".my-agent/logs"
self.max_age_days = max_age_days
def parse_all(self) -> list[AgentEvent]:
entries = []
# Parse your agent's log files and convert them to AgentEvent objects
return entries- Export it from
adr_sensor/parsers/__init__.py, then register it inadr_sensor/observer.pyby constructing it asself.<source>_parserand adding the source key toAgentObserver.SOURCES:
class AgentObserver:
SOURCES = (
...,
("my_agent", "My Agent"),
)
def __init__(self, ...):
...
self.my_agent_parser = MyAgentParser()ingest_all() walks SOURCES and looks the parser up as self.<source>_parser, so
no per-source branch is needed. If the agent only exists on some operating systems,
add it to PLATFORM_RESTRICTED_SOURCES and it will be skipped elsewhere. The CLI
builds its --source choices from SOURCES, so it picks the new agent up for free.
- Add tests in
tests/.
| Python | 3.9, 3.10, 3.11, 3.12, 3.13 |
| Operating system | macOS, Linux, Windows |
| Dependencies | tabulate, zstandard; OpenTelemetry is an optional otel extra |
Which sources yield data depends on the host OS and on which agents are installed; see the platform column in Supported AI Agents. Sources that cannot run on the current platform are skipped rather than failing.
| Variable | Read by | Effect |
|---|---|---|
CODEX_HOME |
Codex parser | Codex data root containing sessions/ and optional state_*.sqlite catalogs (default ~/.codex) |
COPILOT_HOME |
Copilot parser | Copilot CLI data root containing session-state/ (default ~/.copilot) |
GEMINI_CLI_HOME |
Gemini parser | Parent home containing .gemini/tmp/; on macOS also .cache/.gemini/tmp/ |
DSH_HOME |
DeepSeek Harness parser | Harness data root containing sessions/ (default ~/.dsh) |
XDG_CACHE_HOME |
AgentObserver |
Base for --save-sessions output ($XDG_CACHE_HOME/adr_sensor, default ~/.cache/adr_sensor) |
XDG_DATA_HOME |
opencode parser | Overrides the opencode data directory (default ~/.local/share/opencode) |
OPENCODE_DB |
opencode parser | Overrides the opencode SQLite filename or path (:memory: is ignored) |
APPDATA |
Cursor, Cline, Claude Desktop parsers | Windows roaming app-data root. Consulted first so redirected/roaming profiles resolve correctly (default ~/AppData/Roaming) |
LOCALAPPDATA |
Warp parser | Windows local app-data root, same redirected-profile handling (default ~/AppData/Local) |
Each source is isolated during ingestion. See Sensor health and parser diagnostics for structured logs, partial-failure exit behavior, and monitoring limitations.
ADR Sensor enables detection of:
- Suspicious tool usage - Unusual MCP tools, unauthorized file access, credential exfiltration
- Prompt injection - Malicious content injected into agent conversations
- Supply chain risks - Malicious MCP server configurations, suspicious packages
- Data exfiltration - Sensitive data accessed or transmitted by agents
- Anomalous behavior - Activity outside normal patterns, burst tool usage
# Install dev dependencies
pip install -e ".[dev]"
# Run tests
pytest tests/ -v
# Run tests with coverage
pytest tests/ -v --cov=adr_sensor
# Lint
ruff check adr_sensor/
ruff format adr_sensor/adr-sensor/
├── adr_sensor/
│ ├── __init__.py # Package exports
│ ├── cli.py # CLI entry point
│ ├── observer.py # AgentObserver orchestrator
│ ├── exporters/
│ │ ├── config.py # OTLP/HTTP JSON configuration
│ │ └── opentelemetry.py # OpenTelemetry Logs exporter
│ ├── parsers/
│ │ ├── base_parser.py # Abstract base class
│ │ ├── claude_parser.py
│ │ ├── cursor_parser.py
│ │ ├── cline_parser.py
│ │ ├── claude_desktop_parser.py
│ │ ├── codex_parser.py
│ │ ├── copilot_parser.py
│ │ ├── dsh_parser.py
│ │ ├── gemini_parser.py
│ │ ├── opencode_parser.py
│ │ └── warp_parser.py
│ ├── schemas/
│ │ ├── agent_event_schema.py # AgentEvent, ChatMessage, ToolUsage
│ │ └── system_config_schema.py # SystemConfiguration
│ └── utils/
│ ├── string_utils.py
│ └── timestamp_utils.py
├── tests/
├── examples/
│ └── opentelemetry-config.json
├── CONTRIBUTING.md
├── LICENSE
├── pyproject.toml
└── README.md
Apache License 2.0. See the Sensor license for details.
We welcome contributions! See the Sensor contribution guide for guidelines.
Maintainers can publish tagged releases by following the release guide.
Especially welcome:
- New parsers for additional AI agents
- Detection rules and analysis patterns
- Documentation improvements
- Bug reports and fixes