Skip to content

Latest commit

 

History

History

Folders and files

README.md

ADR Sensor

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

Supported AI Agents

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

Claude Code

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.

Claude Desktop Agent Mode

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.

OpenAI Codex CLI

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.

GitHub Copilot CLI

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.

Cline (Claude Dev)

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

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, or opencode-<channel>.db on non-stable channels. Opened read-only so a running opencode process is never disturbed. A database is used only if it contains the session, message and part tables; otherwise the other candidate databases and then the JSON tree are tried, and a database without those tables is reported as unsupported_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.

DeepSeek Harness

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 dsh

Gemini CLI

The 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-history

Contracts 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.

Google Antigravity

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, thinking text, and tool calls), and model-authored GENERIC output. Thinking text is retained with a [Thinking] label. Session times use the records' created_at or timestamp values, 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 as mcp_<server>_<tool> or <server>:<tool>) are parsed to populate server_name and tool_name with tool_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 marked is_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_days or --all-history to 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

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                        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

Quick Start

Installation

Tagged releases are installed from PyPI:

pip install adr-sensor

Install 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 .

CLI Usage

# 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 debug

Sources 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.

Python API

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 Logs Export

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.

Sensor health and parser diagnostics

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.

Runtime log files

--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.

Output Schema

AgentEvent

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.

session_context

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"]
    }
  }
}

token_usage

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
  }
}

Adding a New Parser

ADR Sensor is designed to be extensible. To add support for a new AI agent:

  1. 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
  1. Export it from adr_sensor/parsers/__init__.py, then register it in adr_sensor/observer.py by constructing it as self.<source>_parser and adding the source key to AgentObserver.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.

  1. Add tests in tests/.

Environment

Runtime support

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.

Environment variables

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.

Security Use Cases

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

Development

# 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/

Project Structure

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

License

Apache License 2.0. See the Sensor license for details.

Contributing

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