Platform Adapters
Platform Adapters
Preflight supports 18 named AI coding platforms plus a generic MCP fallback, each via a PlatformAdapter in src/platforms/. Adapters differ in one fundamental way: what the platform actually exposes to a third-party observer. Some platforms have a real hook/callback mechanism that fires on every built-in tool call; others only support MCP as a client, which means Preflight can see calls the platform’s agent chooses to make to Preflight’s own tools, but never a callback for the platform’s built-in tools (file reads, edits, terminal commands, etc).
This doc is the canonical reference for what each adapter can and can’t observe, how detection and setup actually work, and where the gaps are. It mirrors src/platforms/*.ts and the hook-event handling in src/hooks/collector-script.ts — if you change either, update this doc in the same PR.
Maintenance note: every adapter implements
getHookInstallInstructions(), which returns the setup text reproduced below. That method is not currently called from any CLI command (preflight doctor --platform <x>explicitly skips it and tells the user to verify manually) — this document is presently the only place that text is surfaced to a user. If a CLI surface is added later, keep this doc and the adapter methods in sync, or replace the relevant section here with a pointer to the command output.
Integration mechanisms
| Mechanism | Platforms | What’s captured | visibilityLevel |
|---|---|---|---|
Uniform hook events (tool_name/tool_input, PreToolUse/PostToolUse-shaped, case-insensitive event name) |
Claude Code, Kiro, Amazon Q, Droid, Codex, opencode, Kilo Code, Pi1, GitHub Copilot2, GitHub Copilot SDK, GitHub Copilot app | All built-in tool calls | full-hooks |
| Own event names, Claude-Code-shaped fields3 | Gemini CLI | All built-in tool calls (and third-party MCP tool calls) | full-hooks |
Platform-specific hook events (own field vocabulary, own branches in collector-script.ts) |
Cursor, Windsurf, Antigravity4 | All built-in tool calls | full-hooks |
| MCP-client-only (no hook/callback mechanism exists) | Zed, Continue.dev, Cline | Only calls routed to Preflight’s own MCP tools — not the platform’s built-in tools | mcp-tools-only |
| Self-report via MCP tools | Generic MCP fallback | Whatever the caller reports via nr_observe_report_tool_call |
self-reported |
Every PlatformAdapter (src/platforms/types.ts) declares a visibilityLevel field encoding this table in code, not just prose — full-hooks (automatic, deterministic capture), self-reported (built-in-tool-shaped events are observable, but only if an external party — a third-party extension, or the calling MCP client itself — actually reports them), or mcp-tools-only (structurally cannot see built-in tool calls at all). Consumers that blend metrics across platforms (nr_observe_get_platform_comparison, the weekly digest’s per-platform breakdown) use getPlatformVisibilityMap() (src/platforms/platform-registry.ts) to tag results and caveat comparisons that span more than one level.
Detection order matters: createDefaultRegistry() (src/platforms/platform-registry.ts) registers adapters in a fixed order — Claude Code, Cursor, Windsurf, Copilot, Copilot App, Copilot SDK, Zed, Continue, Amazon Q, Kiro, Droid, Gemini CLI, Cline, Codex, opencode, Kilo Code, Pi, Antigravity, then the generic MCP fallback (always last, isSupported() always true). PlatformRegistry.detect() returns the first adapter whose isSupported() returns true; there is no NEW_RELIC_AI_PLATFORM-driven override for most platforms (see per-platform detection below).
Claude Code (claude-code)
Mechanism: Native PreToolUse/PostToolUse/PostToolUseFailure hooks, installed by Preflight itself, plus six separate top-level settings.json hooks keys (not PostToolUse payload variants): StopFailure, which feeds ApiFailureTracker with model-API-call failures; SessionStart, which fires on every session but is only actionable when Claude Code reports resume-cost fields (source: 'resume'/'fork' with a prior response) — those feed SessionResumeTracker, surfaced in nr_observe_get_cost_forecast’s resumeContext; InstructionsLoaded, which feeds InstructionDriftTracker the exact moment a CLAUDE.md/.claude/rules/*.md file enters context — including session-start eager loads, which have no visible Read tool call at all; PostModelSwitch, which feeds ModelUsageTracker a discrete switch event (deliberate /model changes, and persistent automatic changes tagged source: 'auto') — PreModelSwitch is intentionally not installed, since Preflight has no reason to block or confirm a switch; and UserPromptSubmit/Stop, which feed precise turn/task-boundary timestamps into TurnTracker.finalizeTurnAt()/TaskDetector.startTaskIfNone()/TaskDetector.markBoundary() — corroborating signals for the existing idle-gap heuristics, not replacements (Stop doesn’t fire on a user interrupt, so the heuristics stay as the fallback). No prompt/response content is captured from either hook, only timestamp and session ID.
Detection (isSupported()): any of CLAUDECODE, CLAUDE_CODE_ENTRYPOINT, or CLAUDE_CODE_SESSION_ID set (the vars current Claude Code actually sets in child process envs), or the legacy CLAUDE_CODE/CLAUDE_CODE_VERSION, or MCP_CLIENT === 'claude-code'. The list is the exported CLAUDE_CODE_ENV_SIGNALS const, shared with collector-script.ts’s hook-time platform stamping.
Setup:
- Run
npx preflight install - This adds
PreToolUse/PostToolUsehooks to~/.claude/settings.json - Restart Claude Code to activate the hooks
- Add the MCP server to your
.mcp.jsonconfiguration
Notes: The default, first-class platform. Tool names pass through unmapped (mapToolName() is identity).
Cursor (cursor)
Mechanism: Cursor’s own hooks system (.cursor/hooks.json), a local process protocol unrelated to MCP. Confirmed against Cursor’s docs and a Cursor engineer’s forum reply (forum.cursor.com/t/cursor-cli-doesnt-send-all-events-defined-in-hooks/148316).
Detection (isSupported()): CURSOR_SESSION_ID set, or CURSOR_TRACE_ID set, or MCP_CLIENT === 'cursor'.
Two distinct event vocabularies handled by collector-script.ts:
- Per-action hook events —
beforeShellExecution,afterShellExecution,beforeMCPExecution,afterMCPExecution,beforeReadFile,afterFileEdit— carry no generictool_namefield; the collector derives the tool name from the event name itself.CURSOR_TOOL_MAPis never consulted for these. - Generic
preToolUse/postToolUseevents (confirmed to exist, payload only partially documented) — carrytool_nameas one ofShell/Read/Write/Task/MCP.MCPis deliberately left unmapped (collapsing an arbitrary downstream MCP tool into the literal string “MCP” would discard information); it falls through to'Unknown'with the original name preserved.
Known gaps: Cursor has no afterReadFile event (beforeReadFile is emitted as a completed read directly) and no beforeFileEdit event (afterFileEdit is post-only). afterShellExecution/afterMCPExecution have no documented failure-outcome field, so success is reported unconditionally true.
Setup:
- Register the Preflight MCP server for
nr_observe_*tools: Cursor Settings → MCP → add server, commandnpx preflight --stdio, envNEW_RELIC_LICENSE_KEY,NEW_RELIC_ACCOUNT_ID - Configure Cursor hooks so tool-call activity is captured — create
.cursor/hooks.json(project) or~/.cursor/hooks.json(global):{"version": 1,"hooks": {"beforeShellExecution": [{ "command": "preflight-collector" }],"afterShellExecution": [{ "command": "preflight-collector" }],"beforeMCPExecution": [{ "command": "preflight-collector" }],"afterMCPExecution": [{ "command": "preflight-collector" }],"beforeReadFile": [{ "command": "preflight-collector" }],"afterFileEdit": [{ "command": "preflight-collector" }]}} - Ensure
preflight-collectoris onPATH(npm link, ornpm install -g @newrelic/preflight) - Restart Cursor
Windsurf (windsurf)
Mechanism: Windsurf’s real Cascade Hooks system (.windsurf/hooks.json) — docs.windsurf.com/windsurf/cascade/hooks. Not a file watcher or extension API. Windsurf also supports MCP natively via mcp_config.json.
Detection (isSupported()): WINDSURF_SESSION_ID set, or WINDSURF_CONTEXT_ID set, or MCP_CLIENT === 'windsurf'.
Event vocabulary handled by collector-script.ts: pre_read_code, post_read_code, pre_write_code, post_write_code, pre_run_command, post_run_command. Windsurf sends the event name as agent_action_name, not hook_event_name — the collector checks both.
Known gaps: post_read_code/post_run_command report success true unconditionally, the same gap as Cursor’s afterShellExecution. pre_write_code maps to 'Edit' (it’s typically a partial edit, not a full-file write).
Setup:
- Windsurf Settings → MCP Servers → add server, command
npx preflight --stdio, envNEW_RELIC_LICENSE_KEY,NEW_RELIC_ACCOUNT_ID - MCP tool calls via Cascade are captured automatically through the MCP connection
- Built-in tool calls (file reads/writes, terminal commands) require Cascade Hooks — create
.windsurf/hooks.jsonand registerpre_read_code,post_read_code,pre_write_code,post_write_code,pre_run_command,post_run_command, each runningpreflight-collector - See docs.windsurf.com/windsurf/cascade/hooks for the full schema
Zed (zed)
Mechanism: None for built-in tools. Zed’s native agent has no hook/callback mechanism for tool-call interception — confirmed via zed.dev/docs/ai/mcp.html: Zed supports only MCP’s Tools and Prompts features, with no notification for host-side tool calls. As a Zed MCP “context server,” Preflight can only receive calls Zed’s agent makes to Preflight’s own exposed tools.
Detection (isSupported()): ZED_SESSION_ID set, or ZED_EXTENSION_API_VERSION set, or MCP_CLIENT === 'zed', or ZED_ITEM_ID set.
Tool-map status: ZED_TOOL_MAP (real built-in agent tool names from zed.dev/docs/ai/tools.html) exists for correctness and any future hook capability — it is currently unreachable, since no Zed event reaches it. diagnostics, copy_path, move_path, create_directory are real Zed tools deliberately left unmapped.
Workaround: When Zed runs another already-supported platform (Claude Code, Cursor, etc.) as an External Agent via the Agent Client Protocol, that agent’s own native hooks capture its tool calls independently of Zed.
Setup:
- There is no hook mechanism for tool-call capture in Zed’s native agent — Preflight only sees calls made to its own MCP tools.
- To use Preflight as an MCP context server (for its own observability tools): Settings → AI → MCP Servers → Add Server:
{"context_servers": {"preflight": {"command": "npx","args": ["preflight", "--stdio"],"env": {"NEW_RELIC_LICENSE_KEY": "<your-key>","NEW_RELIC_ACCOUNT_ID": "<your-account-id>"}}}}
- For full tool-call observability, run an already-supported platform as a Zed External Agent instead.
Continue.dev (continue)
Mechanism: None for built-in tools. Continue’s native agent (VS Code/JetBrains extension and CLI) has no PreToolUse/PostToolUse-style hook mechanism. Continue supports MCP only as a client.
Detection (isSupported()): CONTINUE_SESSION_ID set, or CONTINUE_SERVER_HOST set, or MCP_CLIENT === 'continue', or MCP_CLIENT_NAME === 'continue'.
Tool-map status: CONTINUE_TOOL_MAP covers Continue’s real built-in tool vocabulary (read_file, edit_existing_file, run_terminal_command, etc.) but, like Zed’s map, is currently unreachable via any hook — it exists for the events that would arrive if Continue ever exposed a callback.
Setup:
- Continue cannot observe built-in tool calls the way Claude Code can — only calls Continue routes to its own MCP tools are visible.
- Create
.continue/mcpServers/preflight.yaml:name: Preflight mcpServerversion: 0.0.1schema: v1mcpServers:- name: preflightcommand: npxargs: ['preflight', '--stdio']env:NEW_RELIC_LICENSE_KEY: <your-key>NEW_RELIC_ACCOUNT_ID: <your-account-id> - Reload Continue.
Note: the continuedev/continue repository is no longer actively maintained and is read-only as of its final 2.0.0 release, so a deeper hook integration is unlikely to land upstream.
Cline (cline)
Mechanism: None for built-in tools on Cline’s VS Code/JetBrains extension — its own docs state Plugins/Hooks and Custom Tools are “not applicable on VSCode and JetBrains Extension for now” (docs.cline.bot/customization/plugins, docs.cline.bot/tools-reference/all-cline-tools) — only the Cline SDK, CLI, and Kanban support the beforeTool/afterTool lifecycle hooks documented at docs.cline.bot/sdk/plugins. Cline also does not forward ambient environment variables into an MCP server’s subprocess automatically — only vars explicitly listed in that server’s own env config block reach it. As a Cline MCP server, Preflight can only receive calls Cline’s agent makes to Preflight’s own exposed tools.
Detection (isSupported()): MCP_CLIENT === 'cline' or NEW_RELIC_AI_PLATFORM === 'cline' — explicit opt-in only, since no ambient env var is set on the MCP server process.
Tool-map status: CLINE_TOOL_MAP covers only execute_command, read_file, and replace_in_file — the three tool names docs.cline.bot/tools-reference/all-cline-tools literally names in its “Legacy Tool Names vs Current Runtime Tools” section. It exists for correctness and any future hook capability — it is currently unreachable, since no Cline event reaches it. Other real or reported Cline tool names (write_to_file, search_files, list_files, browser_action, use_mcp_tool, ask_followup_question, attempt_completion, new_task, plan_mode_respond, etc.) are left unmapped rather than guessed at without a documented source.
Setup:
- There is no hook mechanism for tool-call capture in Cline’s VS Code/JetBrains extension — Preflight only sees calls made to its own MCP tools.
- Extension: open the Cline panel → MCP Servers icon → Configure tab → “Configure MCP Servers”, and add to
mcpServers:{"mcpServers": {"preflight": {"command": "npx","args": ["preflight", "--stdio"],"env": {"MCP_CLIENT": "cline","NEW_RELIC_LICENSE_KEY": "<your-key>","NEW_RELIC_ACCOUNT_ID": "<your-account-id>"}}}} - CLI: edit
~/.cline/mcp.jsonwith the same shape, or runcline mcp. - Restart Cline / reload the extension.
Amazon Q Developer CLI (amazon-q)
Mechanism: A genuine hook mechanism — agentSpawn/userPromptSubmit/preToolUse/postToolUse/stop, configured per-agent. Once wired, Amazon Q’s preToolUse/postToolUse events are handled by the same generic branches in collector-script.ts that Claude Code and Kiro use (identical hook_event_name/tool_name/tool_input field names). Also supports MCP as a client.
Detection (isSupported()): AMAZON_Q_SESSION_ID set, or Q_DEVELOPER_SESSION set, or MCP_CLIENT === 'amazon-q', or AWS_CODEWHISPERER_SESSION set.
Tool-map: Amazon Q CLI has exactly 9 built-in tools; only 4 have a genuine Claude Code equivalent (fs_read→Read, fs_write→Write, execute_bash→Bash, todo_list→TaskCreate). introspect, report_issue, knowledge, thinking, and use_aws are deliberately left unmapped and fall through to 'Unknown' with the original name preserved.
Known gap: Amazon Q hook events carry no session identifier at all (unlike Claude Code, Kiro, Cursor, or Windsurf) — concurrent Amazon Q sessions on the same machine share a single unscoped buffer.
Setup:
- Open your Amazon Q MCP config (
~/.aws/amazonq/mcp.jsonor project-level.amazonq/mcp.json), add tomcpServers:{"preflight": {"command": "npx","args": ["preflight", "--stdio"],"env": {"NEW_RELIC_LICENSE_KEY": "<your-key>","NEW_RELIC_ACCOUNT_ID": "<your-account-id>"}}} - Configure hooks in your agent config (
~/.aws/amazonq/cli-agents/<agent-name>.jsonglobal, or.amazonq/cli-agents/<agent-name>.jsonworkspace):See aws.github.io/amazon-q-developer-cli/agent-format.html#hooks-field.{"hooks": {"preToolUse": [{ "command": "preflight-collector" }],"postToolUse": [{ "command": "preflight-collector" }]}} - Restart Amazon Q Developer CLI (or start a new
q chatsession).
Amazon Kiro (kiro)
Mechanism: MCP stdio protocol, plus real hook events — kiro.dev/docs/cli/hooks. Kiro’s docs describe the event name in lower-camelCase (preToolUse), but a live install (42.08, macOS) was observed sending PascalCase (PreToolUse/PostToolUse), same as Claude Code. collector-script.ts matches case-insensitively, so both spellings work and neither needs a dedicated branch — but don’t rely on the camelCase form being what actually arrives.
Observed payload shape (captured live, 42.08 macOS): top-level keys are session_id, hook_event_name, cwd, tool_name, tool_input, and tool_response on the post event. Note what’s absent — there is no tool_use_id, so pre/post pairing relies on the collector’s own ordering rather than an id echoed back by the platform.
Detection (isSupported()): KIRO_SESSION_ID set, or KIRO_IDE set, or MCP_CLIENT === 'kiro', or NEW_RELIC_AI_PLATFORM === 'kiro'.
Known gap — detection needs an explicit opt-in in practice. Verified against a live Kiro install (42.08, macOS): Kiro passes a Power’s MCP server 16 environment variables and none are Kiro-specific (PWD, INIT_CWD, PLUGIN_ROOT, PLUGIN_DATA, NODE, PATH, HOME, SHELL, USER, LOGNAME, EDITOR, COLOR, SHLVL, _, __CF_USER_TEXT_ENCODING, plus whatever mcp.json declares). So the first three signals above never fire on their own and detection falls through to the generic MCP adapter, which records platform: "generic-mcp" and leaves tool names unmapped — quietly zeroing every metric keyed on a normalized name while the raw tool-call count still looks right. Set NEW_RELIC_AI_PLATFORM: "kiro" in the MCP server’s env to force it; kiro-power/mcp.json does this, and KIRO_POWER.md explains why.
Tool-map: tool_name may arrive as either a tool’s canonical name (fs_read) or a documented alias (read) — both forms are covered in KIRO_TOOL_MAP. Some entries (fsRead, fsCreate, etc.) have no confirmed source in Kiro’s public docs and are kept as best-effort coverage for IDE-surface tool names — don’t remove them without positive evidence they’re wrong. Entries marked // OBSERVED are different: they were captured from that same live install’s session records, and they are snake_case (read_file, read_files, str_replace, list_directory, grep_search, web_fetch) where the map had previously only guessed camelCase — treat those spellings as authoritative. Kiro’s own meta tools (kiro_powers, update_session_information, createHook) are intentionally left unmapped, since mapping them to a file verb would inflate file metrics with activity that never touched a file.
tool_input field names differ from Claude Code’s — captured live, so these are facts rather than inferences:
| Tool | tool_input |
Note |
|---|---|---|
read_file |
{path, offset, limit} |
file key is path, not file_path; offset/limit arrive as null when unset |
str_replace |
{path, oldStr, newStr, replace_all} |
oldStr/newStr, not old_string/new_string |
kiro_powers |
{action, powerName, serverName, toolName, arguments, steeringFile, skillName} |
meta tool, no file |
collector-script.ts has explicit read_file and str_replace cases for this, which is what makes unique_files_read / unique_files_modified non-zero on Kiro. They are separate cases rather than a generic path → file_path promotion because Grep/Glob also send path, where it means a search root — promoting that would misreport searches as file access on every platform. Also note Kiro’s path is sometimes workspace-relative and sometimes absolute — both forms were observed in the same session (cloudformation/export-env.sh from str_replace, a full /Users/... path from read_file). Don’t assume either; anything comparing paths across calls has to normalize first, or the same file counts twice.
Remaining gap: bash_calls_by_category is still empty on Kiro. It needs the command string, which lives in execute_bash’s tool_input — a shape not yet captured first-hand, so no case is guessed for it. bash_commands_run is unaffected (it keys on the mapped name alone). The same applies to read_files, list_directory, grep_search and any write/delete tool: they map correctly for counting, but contribute no structured metadata until their input shapes are observed.
Setup:
- Open your Kiro MCP config (
~/.kiro/settings/mcp.jsonuser-level, or.kiro/settings/mcp.jsonworkspace-level), add tomcpServers:{"preflight": {"command": "npx","args": ["preflight", "--stdio"],"env": {"NEW_RELIC_LICENSE_KEY": "<your-key>","NEW_RELIC_ACCOUNT_ID": "<your-account-id>"}}} - Restart Kiro (or reconnect MCP servers from the Kiro MCP panel).
Alternatively, install Preflight as a Kiro Power — it
provisions the same MCP server without manually editing mcp.json, plus a
documented step for wiring Kiro’s native .kiro/hooks/ system
(kiro.dev/docs/hooks/) for automatic capture.
Factory Droid (droid)
Mechanism: Native hooks.json hook system (~/.factory/hooks.json user scope, .factory/hooks.json project scope, or an org-managed policy) — docs.factory.ai/reference/hooks-reference. Droid’s PreToolUse/PostToolUse events send hook_event_name, tool_name, tool_input/tool_response in the same shape Claude Code, Kiro, and Amazon Q use, so they’re handled by the same generic branches in collector-script.ts — no platform-specific parsing was needed. Also supports MCP as a client independently of the hooks system.
Detection (isSupported()): MCP_CLIENT === 'droid', or NEW_RELIC_AI_PLATFORM === 'droid'. Unlike Cursor/Windsurf/Kiro, Factory’s documentation names no ambient environment variable for a Droid-spawned MCP server process (FACTORY_PROJECT_DIR is scoped to hook command subprocesses only) — detection is explicit-opt-in only; don’t invent one.
Tool-map: Droid’s documented PreToolUse/PostToolUse matchers are Task, Execute, Glob, Grep, Read, Edit, Create, FetchUrl, WebSearch. Read, Glob, Grep, Edit already match Preflight’s canonical vocabulary and are listed as explicit identity entries in DROID_TOOL_MAP (there is no pass-through fallback). Task→Agent, Execute→Bash, Create→Write, FetchUrl→WebFetch, WebSearch→WebSearch.
Known gap: collector-script.ts’s per-tool metadata extractors (extractInputMeta/extractOutputMeta) switch on the raw, unmapped tool name written into the buffer — so Droid’s Create/Execute/Task calls don’t get the extra structured fields (content length, command classification, etc.) that Write/Bash/Agent get for Claude Code. Read/Glob/Grep/Edit (matching exactly) are unaffected. Kiro shared this gap until explicit cases were added for its tool names — see the Kiro section.
Setup:
- Add a
PreToolUse/PostToolUsehook pair matching all tools tohooks.json(~/.factory/hooks.jsonor.factory/hooks.json):{"hooks": {"PreToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "preflight-collector" }] }],"PostToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "preflight-collector" }] }]}} - Ensure
preflight-collectoris onPATH(npm link, ornpm install -g @newrelic/preflight) - Register the Preflight MCP server:
droid mcp add preflight "npx preflight --stdio" \--env MCP_CLIENT=droid \--env NEW_RELIC_LICENSE_KEY=<your-key> \--env NEW_RELIC_ACCOUNT_ID=<your-account-id>
- Restart Droid
Google Gemini CLI (gemini-cli)
Mechanism: Native BeforeTool/AfterTool hooks configured in settings.json (.gemini/settings.json project scope, ~/.gemini/settings.json user scope, or /etc/gemini-cli/settings.json system scope) — github.com/google-gemini/gemini-cli/blob/main/docs/hooks/reference.md. Gemini CLI’s event names don’t match Claude Code’s (BeforeTool/AfterTool, not PreToolUse/PostToolUse), so collector-script.ts has dedicated branches for them — but the fields inside those events (tool_name, tool_input, tool_response, session_id, transcript_path, cwd) match Claude Code’s shape exactly, so those branches reuse the same field-extraction helpers. Also supports MCP as a client independently of the hooks system.
Detection (isSupported()): MCP_CLIENT === 'gemini-cli', or NEW_RELIC_AI_PLATFORM === 'gemini-cli'. Like Droid, Gemini CLI’s documentation names real environment variables for hook command subprocesses (GEMINI_PROJECT_DIR, GEMINI_SESSION_ID, GEMINI_CWD, GEMINI_PLANS_DIR — docs/hooks/index.md) but none confirmed for the MCP-server subprocess itself (docs/tools/mcp-server.md’s environment-sanitization section documents only explicit env overrides) — detection is explicit-opt-in only.
Success/failure signal: Unlike Kiro/Amazon Q’s tool_response.success boolean, Gemini CLI’s AfterTool event has no success field at all — failure is signaled by the presence of tool_response.error. collector-script.ts’s aftertool branch derives success from that field’s presence rather than reusing the posttooluse branch’s boolean lookup.
Stdout contract: Gemini CLI requires hook stdout to be either empty (treated as a parse failure that falls back to “Allow” with a warning) or valid JSON — every one of its own example hooks prints {} before exiting 0, even for pure side-effect hooks (docs/hooks/index.md’s “Strict JSON requirements”). collector-script.ts writes {}\n to stdout after processing a hook, gated to Gemini CLI only (isSupported()’s same env check) — no other platform’s collector behavior changes.
Tool-map: Gemini CLI’s documented built-in tools (docs/tools/file-system.md, docs/tools/shell.md, docs/tools/web-search.md, docs/tools/web-fetch.md): read_file→Read, write_file→Write, replace→Edit (Gemini CLI’s edit tool is named replace, not edit), run_shell_command→Bash, glob→Glob, grep_search→Grep, google_web_search→WebSearch, web_fetch→WebFetch. list_directory has no Claude Code equivalent and is deliberately left unmapped, falling through to 'Unknown' with the original name preserved. There is no Task/Agent-equivalent subagent-dispatch tool anywhere in Gemini CLI’s built-in tool set.
Known gaps: collector-script.ts’s per-tool metadata extractors (extractInputMeta/extractOutputMeta) switch on the raw, unmapped tool name — so Gemini CLI’s replace/run_shell_command/read_file calls don’t get the extra structured fields (old/new string lengths, exit codes, etc.) that Edit/Bash/Read get for Claude Code, the same situation Droid and Kiro are already in. In particular, run_shell_command’s exit code and output live inside tool_response.llmContent, whose internal structure isn’t part of Gemini CLI’s documented public contract, so no attempt is made to parse it.
Setup:
- Add a
BeforeTool/AfterToolhook pair matching all tools tosettings.json(~/.gemini/settings.jsonuser scope or.gemini/settings.jsonproject scope):{"hooks": {"BeforeTool": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "preflight-collector" }] }],"AfterTool": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "preflight-collector" }] }]}} - Ensure
preflight-collectoris onPATH(npm link, ornpm install -g @newrelic/preflight) - Register the Preflight MCP server:
gemini mcp add preflight "npx preflight --stdio" \-e MCP_CLIENT=gemini-cli \-e NEW_RELIC_LICENSE_KEY=<your-key> \-e NEW_RELIC_ACCOUNT_ID=<your-account-id>
- Restart Gemini CLI
OpenAI Codex (codex)
Mechanism: Native hooks.json lifecycle-hooks system (~/.codex/hooks.json user scope, <repo>/.codex/hooks.json project scope, or inline [hooks] tables in config.toml) — developers.openai.com/codex/hooks. Codex’s PreToolUse/PostToolUse events send hook_event_name, tool_name, tool_input/tool_response, session_id, cwd, transcript_path, tool_use_id, permission_mode in the same shape Claude Code, Kiro, Amazon Q, and Droid use, so they’re handled by the same generic branches in collector-script.ts — no platform-specific parsing was needed. Also supports MCP as a client independently of the hooks system (developers.openai.com/codex/extend/mcp). Non-managed hooks (anything outside a system/MDM/requirements.toml-managed source) require one-time review and trust via /hooks in the Codex CLI before they run.
Detection (isSupported()): MCP_CLIENT === 'codex', or NEW_RELIC_AI_PLATFORM === 'codex'. Checked Codex’s full documented environment-variable list (developers.openai.com/codex/config-file/environment-variables) — CODEX_HOME, CODEX_SQLITE_HOME, CODEX_NON_INTERACTIVE, CODEX_INSTALL_DIR, CODEX_API_KEY, CODEX_ACCESS_TOKEN, CODEX_CA_CERTIFICATE, RUST_LOG — none are documented as injected into an MCP server subprocess’s environment as an ambient “running under Codex” signal. CLAUDE_PLUGIN_ROOT/CLAUDE_PLUGIN_DATA are real but scoped to plugin-bundled hook command subprocesses specifically, not to an MCP server started via codex mcp add — detection is explicit-opt-in only, same as Droid/Gemini CLI/Cline.
Tool coverage, confirmed via Codex’s own tool-coverage table: shell commands and unified exec (exec_command) are both fully covered (Yes/Yes for PreToolUse/PostToolUse), reported with the canonical literal tool_name "Bash" — no translation needed. apply_patch is also fully covered but always reports tool_name literally as "apply_patch" (never split into Edit/Write, even though Codex’s own hook matcher config lets a user alias-match it as either); CODEX_TOOL_MAP collapses it to 'Edit', a single value, the same treatment Windsurf gives pre_write_code. MCP tool calls and other local function tools (e.g. spawn_agent, matched by the coverage table as also aliasable to Agent) are fully covered too. update_plan has no confirmed Preflight canonical equivalent and is left unmapped.
Known gaps:
- Hosted tools are entirely unobservable. Codex’s own tool-coverage table marks hosted tools such as
WebSearchas No/No for bothPreToolUseandPostToolUse— “these don’t use the local function-tool hook path.” Preflight cannot see these calls at all, regardless of hook configuration. apply_patchcalls don’t get Edit’s structured metadata.collector-script.ts’sextractInputMeta/extractOutputMetaswitch on the raw tool name ("apply_patch", not"Edit") beforemapToolName()ever runs, so Codex’sapply_patchcalls don’t get theoldStringLength/newStringLength/etc. fields Claude Code’sEditcalls get — the same pre-existing situation Gemini CLI’sreplaceand Droid’sCreate/Execute/Taskare already in.- A
write_stdinpoll against an already-open unified-exec session doesn’t re-triggerPreToolUse. This is narrower than it may sound:exec_commanditself is fully covered (Yes/Yes) — only a follow-up poll of a session that already passedPreToolUseis exempt from running it again.
Setup:
- Add a
PreToolUse/PostToolUsehook pair matching all tools tohooks.json(~/.codex/hooks.jsonuser scope or<repo>/.codex/hooks.jsonproject scope):{"hooks": {"PreToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "preflight-collector" }] }],"PostToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "preflight-collector" }] }]}} - Run
/hooksin the Codex CLI to review and trust this hook definition — non-managed hooks are skipped until reviewed. - Ensure
preflight-collectoris onPATH(npm link, ornpm install -g @newrelic/preflight) - Register the Preflight MCP server:
codex mcp add preflight --env MCP_CLIENT=codex \--env NEW_RELIC_LICENSE_KEY=<your-key> \--env NEW_RELIC_ACCOUNT_ID=<your-account-id> \-- npx preflight --stdio
- Restart Codex
opencode (opencode)
Mechanism: Unlike every other full-hooks platform above, opencode has no external hooks.json — its only interception point is an in-process JS/TS plugin loaded from .opencode/plugins/ (project) or ~/.config/opencode/plugins/ (global), or via an npm package listed in opencode.json’s plugin array (opencode.ai/docs/plugins/). Preflight ships a documented plugin snippet (see Setup below) that translates opencode’s real tool.execute.before/tool.execute.after hook payloads — confirmed from the published @opencode-ai/plugin npm package’s own type definitions: input: {tool, sessionID, callID} / output: {args} for before, input: {tool, sessionID, callID, args} / output: {title, output, metadata} for after — into Claude Code’s PreToolUse/PostToolUse JSON shape before piping to preflight-collector. collector-script.ts’s existing generic branches handle it as a result — no changes to that file were needed.
Detection (isSupported()): MCP_CLIENT === 'opencode', or NEW_RELIC_AI_PLATFORM === 'opencode'. Checked opencode’s full documented local-MCP-server option set (opencode.ai/docs/mcp-servers/) — type, command, cwd, environment, enabled, timeout — nothing is documented as an ambient “running under opencode” signal injected into a spawned local MCP server’s environment. Detection is explicit-opt-in only, same as Droid/Gemini CLI/Cline/Codex.
Tool coverage, confirmed via opencode’s own tools doc (opencode.ai/docs/tools/): bash, read, write, edit, grep, glob, webfetch, websearch (gated behind OPENCODE_ENABLE_EXA or the OpenCode-hosted provider) map directly. apply_patch collapses to 'Edit' — same “no clean 1:1, multi-file patch” reasoning as CodexAdapter. skill maps to 'Skill' (same precedent as ContinueAdapter’s read_skill). todowrite maps to 'TaskCreate' (same precedent as AmazonQAdapter’s todo_list). question maps to 'AskUserQuestion' (opencode’s own docs describe a header + question text + options list, matching that tool’s shape). lsp (experimental, gated behind OPENCODE_EXPERIMENTAL_LSP_TOOL) has no confirmed Preflight canonical equivalent and is left unmapped.
Known gaps:
- No success/error signal exists in opencode’s hook payload at all. Every opencode tool call reports
success: trueunconditionally — same convention as Cursor’safterShellExecution/Windsurf’spost_read_code. A genuinely failed tool call (e.g. non-zero bash exit) is indistinguishable from a successful one through this hook. - Only
bash/read/edit/writeget structured input metadata. The plugin snippet’stoClaudeShape()function special-cases these four, remapping theirargsfields:bash→{ command: args.command }, andread/edit/write→{ file_path: args.filePath }.apply_patch/grep/glob/webfetch/websearch/todowrite/skill/question’sargsare forwarded as-is — forapply_patchspecifically, this means itspatchTextfield never reachescollector-script.ts’sEdit-case extractors (which expectold_string/new_string), so despite being mapped to theEdittool name, it gets no structured metadata. - No output-side metadata at all (
exitCode,editSuccess,grepMatchCount, etc.). opencode’soutput.output/output.metadataare opaque and tool-specific with no documented convention — the plugin snippet’sPostToolUsepayload intentionally omitstool_responserather than guessing a mapping. - Requires a user-installed plugin file, not a JSON hooks config — a genuinely different, code-based setup step from every other platform’s instructions.
Setup:
- Register the Preflight MCP server in
opencode.json:{"mcp": {"preflight": {"type": "local","command": ["npx", "preflight", "--stdio"],"environment": {"MCP_CLIENT": "opencode","NEW_RELIC_LICENSE_KEY": "<your-key>","NEW_RELIC_ACCOUNT_ID": "<your-account-id>"}}}} - Ensure
preflight-collectoris onPATH(npm link, ornpm install -g @newrelic/preflight) - Create
.opencode/plugins/preflight.js(project) or~/.config/opencode/plugins/preflight.js(global) with the snippet inOpencodeAdapter.getHookInstallInstructions()(src/platforms/opencode-adapter.ts) — reproduced there in full. - Restart opencode.
Kilo Code (kilocode)
Mechanism: Kilo CLI (@kilocode/cli) is a confirmed literal fork of opencode — Kilo’s own docs state directly: “The Kilo CLI is a fork of OpenCode.” It shares opencode’s exact interception mechanism: no external hooks.json, only an in-process JS/TS plugin loaded from .kilo/plugin/ (project) or ~/.config/kilo/plugin/ (global) — not .opencode/, since Kilo CLI 1.0 no longer reads legacy .opencode/.kilocode directories (kilocode.ai/docs/automate/extending/plugins). Preflight ships a documented plugin snippet (see Setup below) that translates Kilo’s real tool.execute.before/tool.execute.after hook payloads — confirmed from packages/plugin/src/index.ts in Kilo-Org/kilocode, the actual source backing the published @kilocode/plugin npm package: input: {tool, sessionID, callID} / output: {args} for before, input: {tool, sessionID, callID, args} / output: {title, output, metadata} for after — byte-identical field names to opencode’s confirmed shapes. collector-script.ts’s existing generic branches handle it as a result — no changes to that file were needed.
Detection (isSupported()): MCP_CLIENT === 'kilocode', or NEW_RELIC_AI_PLATFORM === 'kilocode'. Checked Kilo’s full documented “Environment Variable Overrides” set (kilocode.ai/docs/code-with-ai/platforms/cli) — KILO_PROVIDER, KILOCODE_<FIELD>/KILO_<FIELD_NAME>, KILO_ORG_ID, KILO_CONFIG/KILO_CONFIG_CONTENT, KILO_PURE — every one is an optional user-set config override or a plugin-disable switch, none is an ambient “running under Kilo” marker a spawned MCP server process could rely on. Detection is explicit-opt-in only, same as opencode/Droid/Gemini CLI/Cline/Codex.
Tool coverage, confirmed via Kilo’s own tools docs (kilocode.ai/docs/automate/tools, /docs/automate/how-tools-work): read, glob, grep, edit, write, bash, webfetch, websearch map directly. apply_patch collapses to 'Edit' — same precedent as CodexAdapter/OpencodeAdapter. skill maps to 'Skill', question maps to 'AskUserQuestion', and todowrite maps to 'TaskCreate' — all reusing OpencodeAdapter’s precedents directly. task (spawns a sub-agent/child session) maps to 'Agent' — same precedent as Zed/Codex’s spawn_agent and Cursor/Droid’s Task. todoread maps to 'TaskList' and plan maps to 'EnterPlanMode' — both real, pre-existing entries in Preflight’s canonical tool vocabulary with no adapter-map precedent before this one. agent_manager (a VS-Code-specific UI action starting Agent Manager local/worktree sessions) has no clean canonical equivalent and is left unmapped, same treatment as opencode’s lsp and Codex’s update_plan. Kilo’s built-in Playwright MCP server tools (namespaced kilo-playwright_*) are not fixed map keys and fall through to 'Unknown', same as any other platform’s MCP-server tool calls.
Known gaps:
- No success/error signal exists in Kilo’s hook payload at all, same gap as opencode — every Kilo tool call reports
success: trueunconditionally. - Only
bash/read/edit/writeget structured input metadata. The plugin snippet’stoClaudeShape()function special-cases these four.apply_patch/glob/grep/webfetch/websearch/todowrite/todoread/plan/task/skill/question/agent_manager’sargsare forwarded as-is — forapply_patchspecifically, its (unconfirmed but presumed opencode-identical)patchTextfield never reachescollector-script.ts’sEdit-case extractors, so despite being mapped to theEdittool name it gets no structured metadata. - No output-side metadata at all (
exitCode,editSuccess, etc.) —output.output/output.metadataare opaque, so thePostToolUsepayload intentionally omitstool_response. kilo-playwright_*tool calls report as'Unknown'with the dynamic name preserved asplatformToolName— they’re observable through this same plugin mechanism but aren’t translated by the snippet’s static tool map.- Requires a user-installed plugin file, not a JSON hooks config — same setup burden as opencode.
Setup:
- Register the Preflight MCP server in
kilo.json:{"mcp": {"preflight": {"type": "local","command": ["npx", "preflight", "--stdio"],"environment": {"MCP_CLIENT": "kilocode","NEW_RELIC_LICENSE_KEY": "<your-key>","NEW_RELIC_ACCOUNT_ID": "<your-account-id>"}}}} - Ensure
preflight-collectoris onPATH(npm link, ornpm install -g @newrelic/preflight) - Create
.kilo/plugin/preflight.ts(project) or~/.config/kilo/plugin/preflight.ts(global) with the snippet inKiloCodeAdapter.getHookInstallInstructions()(src/platforms/kilo-code-adapter.ts) — reproduced there in full, using Kilo’s current plugin module descriptor shape (export default { id, server }). - Restart Kilo Code.
Pi (pi)
Mechanism: Pi (@earendil-works/pi-coding-agent) ships a native Extension API — TypeScript modules in ~/.pi/agent/extensions/ (global) or .pi/extensions/ (project-local), auto-discovered and hot-reloadable via /reload. Confirmed from docs/extensions.md’s “Tool Events” section (github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md): pi.on("tool_call", (event, ctx) => {...}) fires before every built-in or custom tool executes, exposing event.toolName/event.toolCallId/mutable event.input, and can block via return { block: true, reason? }. pi.on("tool_result", (event, ctx) => {...}) fires after, exposing event.content/event.details/event.isError/event.usage, and can modify the result. Preflight ships a documented extension snippet (see Setup below) that translates these into Claude Code’s exact PreToolUse/PostToolUse hook JSON shape before piping to preflight-collector — collector-script.ts’s existing generic branches handle it as a result, no changes to that file were needed.
Pi has no MCP client support at all, confirmed as deliberate, stated philosophy on pi.dev and in the README’s “Philosophy” section: “No MCP. Build CLI tools with READMEs (see Skills), or build an extension that adds MCP support.” This makes Pi’s setup path structurally different from every other full-hooks adapter: there is no MCP server registration step, and --stdio mode (the mode every other full-hooks adapter pairs with) is categorically unusable. Setup instead uses Preflight’s --local mode (ARCHITECTURE.md: “Collection plane + Processing pipeline + Dashboard HTTP server. No MCP connection”) plus the existing macOS dashboard daemon (installDashboardDaemon() in src/install/schedule.ts) to keep a --local process running persistently, since nothing else spawns or owns one for Pi sessions.
Detection (isSupported()): PI_CODING_AGENT === 'true' — confirmed from the README’s Environment Variables table: “Set to true by the CLI and RPC entry points so child processes can detect that they run inside Pi.” A real, documented, first-party ambient signal — not an invented one, and no explicit-opt-in fallback is needed.
Tool coverage, confirmed from the README’s CLI Reference (Available built-in tools: read, bash, edit, write, grep, find, ls): bash/read/write/edit/grep map directly. find maps to 'Glob' — same precedent as Gemini CLI’s glob → 'Glob'. ls has no confirmed canonical Preflight tool-name equivalent and is left unmapped → falls through to 'Unknown', same treatment as ZedAdapter’s create_directory/move_path. Only read/write/edit/bash are enabled by default — grep/find/ls are real built-ins but require explicit --tools enabling to produce any events.
Known gaps:
- No MCP client support exists in Pi at all, by the platform’s own explicit design choice — this adapter’s setup path (
--localmode + the macOS dashboard daemon) is structurally different from every other adapter’s “register the MCP server + install a hook” two-step. - The persistent-process daemon is macOS-only.
installDashboardDaemon()installs alaunchdLaunchAgent; no systemd unit or Windows Task Scheduler equivalent exists in this codebase today. Linux/Windows users must keeppreflight --localrunning manually. lshas no confirmed canonical Preflight tool-name equivalent and reports as'Unknown'.grep/find/lsare disabled by default — they only produce events in sessions that explicitly enable them via--tools.event.input’s exact per-tool field shapes are confirmed only forbashandreadindocs/extensions.md’s own example. The extension snippet forwardsevent.inputas-is for every tool rather than remapping field names, socollector-script.ts’s tool-specific input extractors (which expect Claude Code’s own field names, e.g.file_path) won’t populate for Pi’s calls — an honest gap, not a broken mapping.
Setup:
- Run
preflight setuponce to configureNEW_RELIC_LICENSE_KEY/NEW_RELIC_ACCOUNT_IDand, on macOS, install the background dashboard daemon that keeps a--localprocess running persistently. - On Linux/Windows (no daemon support yet): run
preflight --local &yourself in a persistent terminal/tmux session and keep it running. - Ensure
preflight-collectoris onPATH(npm link, ornpm install -g @newrelic/preflight). - Create
~/.pi/agent/extensions/preflight.ts(global) or.pi/extensions/preflight.ts(project) with the snippet inPiAdapter.getHookInstallInstructions()(src/platforms/pi-adapter.ts) — reproduced there in full. - Restart Pi (or run
/reloadin an active session).
Google Antigravity (antigravity)
Mechanism: Google Antigravity (2.0 / IDE / CLI — the Python SDK is out of scope, see Known gaps) ships a real, first-party, documented hooks.json mechanism: antigravity.google/docs/hooks (identical content on /docs/ide/hooks). PreToolUse/PostToolUse fire around every built-in tool call, matched by regex against tool name; handlers receive JSON on stdin and must reply with JSON on stdout. This is a full-hooks mechanism, not the mcp-tools-only tier a platform lacking any hook mechanism would get — Antigravity’s hooks.json is real, first-party, and documented, confirmed by fetching Google’s own docs directly. Antigravity also supports MCP separately and natively — /docs/mcp — via mcp_config.json.
Antigravity’s hook payloads have no field naming the event type at all — PreToolUse’s payload is { toolCall: { name, args }, stepIdx, conversationId, ... }; PostToolUse’s is { stepIdx, error, conversationId, ... }, with no tool-name field whatsoever. collector-script.ts dispatches on the presence of toolCall instead. Because PostToolUse never carries a tool name, both events set toolUseId from stepIdx so HookEventProcessor pairs them by ID rather than its tool-name-FIFO fallback — the merged record’s toolName comes from the matched pre-event (confirmed by reading HookEventProcessor.handlePostEvent() directly), so the post-event’s placeholder 'unknown' tool name is never surfaced for a successfully-paired call.
Detection (isSupported()): MCP_CLIENT === 'antigravity'. No ambient environment variable is confirmed to exist for Antigravity (checked /docs/cli/reference, /docs/cli/settings, /docs/ide/settings, /docs/cli/sandbox, /docs/sidecars) — explicit opt-in only, same as opencode/Kilo Code/Codex.
Tool coverage, confirmed from the Hooks page’s “Supported Tools” section: view_file→Read, write_to_file→Write, replace_file_content→Edit, multi_replace_file_content→MultiEdit, list_dir/find_by_name→Glob, grep_search→Grep, search_web→WebSearch, read_url_content→WebFetch, run_command→Bash, invoke_subagent→Agent. manage_task, schedule, list_permissions, ask_permission, define_subagent, send_message, manage_subagents, ask_question, and generate_image are real Antigravity built-ins with no canonical Preflight equivalent, deliberately left unmapped.
Known gaps:
PreToolUse’stoolCall.argsfield names (CommandLine,TargetFile,ReplacementChunks, etc.) are not remapped throughextractInputMeta()’s tool-specific extractors —toolInputmetadata is absent for Antigravity calls until that mapping is added; an honest gap, not a guess.PostToolUsecarries no output content/exit-code field at all (onlystepIdx/error) —toolOutputmetadata (e.g. Bash exit codes) can never be populated for Antigravity, a platform limitation, not an implementation gap.- The Antigravity SDK (Python, for building custom agents on Antigravity’s own runtime) is out of scope — it documents MCP support but no interactive hooks surface of its own.
- No confirmed ambient env var — detection is explicit-opt-in only (
MCP_CLIENT=antigravity).
Setup:
- Register Preflight as an MCP server for its own
nr_observe_*tools — add tomcp_config.json(global~/.gemini/config/mcp_config.json, or per-workspace.agents/mcp_config.json):{"mcpServers": {"preflight": {"command": "npx","args": ["preflight", "--stdio"],"env": {"MCP_CLIENT": "antigravity","NEW_RELIC_LICENSE_KEY": "<your-key>","NEW_RELIC_ACCOUNT_ID": "<your-account-id>"}}}} - Capture built-in tool calls via
hooks.json(same global/workspace locations):{"preflight": {"PreToolUse": [{ "matcher": "*", "hooks": [{ "command": "preflight-collector" }] }],"PostToolUse": [{ "matcher": "*", "hooks": [{ "command": "preflight-collector" }] }]}} - Ensure
preflight-collectoris onPATH(npm link, ornpm install -g @newrelic/preflight). - Restart Antigravity (or reload the workspace).
GitHub Copilot (copilot)
Mechanism: VS Code agent hooks (code.visualstudio.com/docs/copilot/customization/hooks, Preview) — PreToolUse/PostToolUse (plus 6 other lifecycle events) fire on every built-in tool call and pass JSON on stdin with the uniform hook_event_name/tool_name/tool_input/tool_use_id/session_id envelope, parsed by collector-script.ts’s uniform branch. This section covers VS Code Copilot Chat only — for the GitHub Copilot CLI/SDK runtime, see the “GitHub Copilot SDK” (copilot-sdk) section below, not this one: Copilot CLI’s native lowerCamelCase hook config (preToolUse) emits a different, incompatible payload shape with no hook_event_name field at all (confirmed against GitHub’s own hooks reference), so it does not “just work” against this adapter’s PascalCase-only setup — only the PascalCase PreToolUse/PostToolUse config the copilot-sdk section documents is supported for that host. VS Code also reads Claude-format hook files (~/.claude/settings.json) by default via chat.hookFilesLocations, so a preflight install done for Claude Code is picked up by VS Code Copilot automatically (matchers are parsed but ignored; Preflight’s matcher is empty anyway).
Documented deltas from Claude Code (hooks FAQ, same page):
tool_inputkeys are camelCase (filePath,oldString), not snake_case — handled inextractInputMeta.- Tool names are VS Code’s own (
create_file,replace_string_in_file,run_in_terminal), not Claude’s (Write,Edit,Bash).COPILOT_TOOL_MAPsources its inventory from theToolNameenum in microsoft/vscodeextensions/copilot/src/extension/tools/common/toolNames.ts;editFilesappears in the hooks reference example payloads. Names with no clear canonical correspondence (semantic_search,get_errors, …) are deliberately unmapped so the original name is preserved. tool_responsecan be a plain string ("File edited successfully"per the hooks reference); success then defaults totrue.
Detection (isSupported()): MCP_CLIENT === 'copilot', or NEW_RELIC_AI_PLATFORM === 'copilot' (the only adapter besides Kiro that actually reads NEW_RELIC_AI_PLATFORM).
Token-exact cost: CopilotUsageWatcher (src/hooks/copilot-usage-watcher.ts) tails VS Code’s per-session Copilot debug log (<userDataDir>/workspaceStorage/<hash>/GitHub.copilot-chat/debug-logs/<sessionId>/main.jsonl), whose llm_request records carry exact inputTokens/outputTokens/cachedTokens per model request, and emits mode: 'token' events into the same session buffer the hooks write to — the Copilot analog of the Claude Code parent-transcript watcher. The path + record schema come from VS Code’s chatDebugFileLoggerService.ts (formerly microsoft/vscode-copilot-chat, now merged into microsoft/vscode under extensions/copilot/ and archived) and its otel-data-flow.html. Both desktop user-data dirs and the VS Code Remote server dirs (~/.vscode-server, ~/.vscode-server-insiders, ~/.vscode-remote) are searched, so WSL/SSH/dev-container sessions are covered. The log format is not a stable API (same stability tier as the Claude Code transcript format the other watchers depend on); schema drift degrades to estimation-based cost, never a crash. Opt out with NR_AI_ENABLE_COPILOT_USAGE_WATCHER=0.
Required setting (prerequisite for token-exact cost): this debug log is off by default, gated behind github.copilot.chat.agentDebugLog.fileLogging.enabled — an advanced/experimental setting hidden from the normal Settings UI and still labelled “(Preview)”. Users must add it to their VS Code User settings.json:
"github.copilot.chat.agentDebugLog.fileLogging.enabled": truethen reload the window (Developer: Reload Window). Until then the debug-logs directory never exists and cost falls back to content-size estimation. CopilotUsageWatcher detects this case (a VS Code workspaceStorage root present but no debug-logs directory anywhere), logs a one-time warning naming the setting, and surfaces it as copilotDebugLoggingDisabled in the dashboard observability-health snapshot — so a zero-cost session reads as a missing prerequisite rather than a broken integration.
Known gaps: agent hooks are a Preview feature and may change; organizations can disable hooks via enterprise policy. Hook matchers are ignored by VS Code, so per-tool matcher filtering is unavailable (irrelevant to Preflight’s empty matcher). Token-exact cost additionally requires the off-by-default github.copilot.chat.agentDebugLog.fileLogging.enabled setting (see above); without it, cost is estimation-based. Cache-creation (cache-write) tokens are a known estimation gap: VS Code’s debug-log schema exposes only a single cache-read cachedTokens figure, so cache-write tokens are folded into base-rate input and slightly under-billed on cache-write turns.
Setup:
- Or just run
preflight setup(orpreflight install --copilotdirectly) — it automates every step below for both VS Code Copilot Chat and the Copilot CLI, including the double-capture fix in step 1b. - Create a hooks file — user-level
~/.copilot/hooks/preflight.json(applies to all workspaces) or workspace-level.github/hooks/preflight.json(both are documented hook locations in VS Code’s location table):The{"version": 1,"hooks": {"PreToolUse": [{"type": "command","command": "NEW_RELIC_AI_PLATFORM=copilot preflight-collector pre-tool"}],"PostToolUse": [{"type": "command","command": "NEW_RELIC_AI_PLATFORM=copilot preflight-collector post-tool"}]}}NEW_RELIC_AI_PLATFORM=copilotprefix is required: Copilot’s hooks-runner process is spawned separately from the MCP server process and does not inherit env vars set on an MCP registration (e.g.copilot mcp add --env MCP_CLIENT=...), so without embedding the tag directly in the command string, every event silently falls through to theclaude-codeplatform default. 1b. VS Code also reads Claude-format hooks from~/.claude/settings.jsonby default, so apreflight installdone for Claude Code is picked up automatically — but hooks from all locations are collected, so same-event hooks in both files double-capture every tool call. The installer (step 0) fixes this automatically by writing"chat.hookFilesLocations": { "<claude-settings-path>": false }into VS Code’ssettings.json; doing it by hand means disabling the Claude location for Copilot the same way. - Ensure
preflight-collectoris onPATH(npm link, ornpm install -g @newrelic/preflight). - Register the Preflight MCP server for
nr_observe_*tools with envMCP_CLIENT=copilot,NEW_RELIC_LICENSE_KEY,NEW_RELIC_ACCOUNT_ID. - For token-exact cost, add
"github.copilot.chat.agentDebugLog.fileLogging.enabled": trueto VS Code Usersettings.jsonand reload the window (see Required setting above). Not needed for tool-call/session telemetry, only for exact cost. - For the GitHub Copilot CLI/SDK runtime instead of VS Code, use the
copilot-sdkadapter’s own setup (see the “GitHub Copilot SDK” section below) — Copilot CLI’s native lowerCamelCase hooks are not compatible with this adapter.
Legacy fallback (HTTP push): the previous integration path remains — a Copilot-compatible VS Code extension pushing events to http://localhost:9847 ("preflight.endpoint" in VS Code settings), with the file_edit/file_open/file_create/file_delete/terminal_command/task event vocabulary. Preflight only receives what such an extension chooses to send; treat that path’s fidelity as bounded by the extension.
GitHub Copilot SDK (copilot-sdk)
Mechanism: The Copilot SDK / agent-host runtime — distinct from the VS Code Copilot Chat adapter above (copilot): different session-id space (~/.copilot/session-state/<id>/, no workspaceStorage) and a different tool-name vocabulary. The GitHub Copilot CLI is the confirmed host of this runtime. The GitHub Copilot desktop app is a confirmed second host — it runs a warm pool of Copilot CLI processes — but it gets its own adapter (copilot-app, see its section below) because two assumptions this section makes for the CLI do not hold there: the app cannot load the .mjs extension mechanism documented under “Token-exact cost” (it is a Rust binary with no extensions/ loading), and ~/.copilot/session-state/<id>/ is not unique to the standalone CLI (the app’s pooled CLI processes create it too). Tool calls arrive via the host’s own PreToolUse/PostToolUse hooks, parsed by preflight-collector (the same uniform hook envelope collector-script.ts already handles for other hook-based platforms).
Detection (isSupported()): MCP_CLIENT === 'copilot-sdk', or NEW_RELIC_AI_PLATFORM === 'copilot-sdk'.
Event vocabulary: the adapter’s setup requires the PascalCase PreToolUse/PostToolUse hook config, under which GitHub’s CLI already canonicalizes tool_name to Claude-shaped names before Preflight ever sees them: bash/powershell→Bash, view→Read, create→Write, edit/str_replace_editor/apply_patch→Edit, grep/rg→Grep, glob→Glob, web_fetch→WebFetch, web_search→WebSearch, ask_user→AskUserQuestion, update_todo→TodoWrite, task→Agent. Source: GitHub’s Copilot hooks reference, “Claude-format matchers (PascalCase PreToolUse)” section. The adapter’s own tool map keeps these canonical names as the primary (identity) lookup and the raw runtime names as a defensive fallback only. Deliberately left unmapped (no Claude equivalent, preserved as-is): the background shell session-management tools (list_bash/read_bash/stop_bash/write_bash and their PowerShell equivalents), list_agents/read_agent/write_agent, skill.
Instruction files: AGENTS.md, CLAUDE.md, GEMINI.md, .github/copilot-instructions.md (repo-relative, matched against a tool call’s file path). The host also reads a user-level $HOME/.copilot/copilot-instructions.md and glob-based .github/instructions/**/*.instructions.md, neither of which is modeled here — every other adapter’s instructionFilePaths entry is a repo-relative exact filename, which supports neither a global per-user path nor a glob pattern.
Token-exact cost (optional): tool-call hooks alone don’t carry per-call token counts. A small, hand-written plain-JavaScript Copilot SDK extension (copilot-sdk-extension/extension.mjs, shipped alongside Preflight and installable into ~/.copilot/extensions/preflight/) subscribes to the SDK’s assistant.usage event and appends a mode: 'token' buffer line per API call — the same buffer-line contract CopilotUsageWatcher uses for VS Code. It captures usage only (no tool calls, to avoid double-counting against the hooks above). Copilot SDK extensions must be plain JavaScript — TypeScript isn’t supported — so this file hand-mirrors the tested logic in src/hooks/copilot-sdk-usage-mapper.ts; keep the two in sync when changing either. Extensions are an experimental feature, requiring --experimental or /experimental on.
Known gap: without the extension above, cost is estimated from tool-call counts rather than exact tokens (same limitation as most other hook-based adapters).
Setup:
- Or just run
preflight setup(orpreflight install --copilotdirectly) — it automates every step below, including the hooks file, MCP registration, and (optionally) the SDK usage extension in step 4. - Create a hooks file for tool-call capture — user-level
~/.copilot/hooks/preflight.jsonor workspace-level.github/hooks/preflight.json:The{"version": 1,"hooks": {"PreToolUse": [{"type": "command","command": "NEW_RELIC_AI_PLATFORM=copilot preflight-collector pre-tool"}],"PostToolUse": [{"type": "command","command": "NEW_RELIC_AI_PLATFORM=copilot preflight-collector post-tool"}]}}NEW_RELIC_AI_PLATFORM=copilotprefix is required — the hooks-runner does not inherit env vars set on the MCP server registration in step 3 below, so without it every event falls through to theclaude-codeplatform default. - Ensure
preflight-collectoris onPATH(npm link, ornpm install -g @newrelic/preflight) - Register the Preflight MCP server:
Terminal window copilot mcp add preflight \--env MCP_CLIENT=copilot-sdk \--env NEW_RELIC_LICENSE_KEY=<your-key> \--env NEW_RELIC_ACCOUNT_ID=<your-account-id> \-- npx preflight --stdio - (Optional, for token-exact cost) Copy the bundled extension to
~/.copilot/extensions/preflight/extension.mjs - Restart the host with
--experimental(or run/experimental onin an already-open session) — extensions load at startup; asking Copilot to reload extensions or running/clearmay also pick up a newly-copied extension
GitHub Copilot app (copilot-app)
Mechanism: The GitHub Copilot desktop app is a Rust GUI over a warm pool of Copilot CLI processes running in server/stdio mode — the app’s own lifecycle logs name the component (github_app::session::manager::cli_pool) and show it spawning CLI processes with enable_config_discovery=true. Every empirical claim in this section was verified live on macOS against Copilot app v1.1.14 (commit 0d498e8, data.db schema v100) on 2026-09-01, except the session-state/ observation dated below. Because the pooled processes are the same Copilot CLI runtime the copilot-sdk adapter targets, a ~/.copilot/hooks/preflight.json with PascalCase PreToolUse/PostToolUse fires on every tool call, with tool_name already canonicalized to Claude-shaped names (Grep, Read, Edit, Bash, Glob all observed live) and full Bash command text included — full-hooks-grade capture, handled by collector-script.ts’s existing uniform branch with no changes to that file needed.
Detection (isSupported()): MCP_CLIENT === 'copilot-app', or NEW_RELIC_AI_PLATFORM === 'copilot-app', or ambient: ~/.copilot/data.db exists and was modified within the last 7 days (directory overridable via NEW_RELIC_AI_COPILOT_DIR, for tests and non-default installs). The recency requirement exists because bare existence is sticky — uninstalling the app leaves data.db behind, and the app writes the file continuously while running, so mtime tracks actual app activity. data.db is created only by the desktop app — the standalone CLI’s documented config dir has no such file, and the app’s ~/.copilot on this host also lacked the CLI’s documented extensions/ and mcp-config.json. One nuance the adapter’s comments spell out: session-state/ is not a usable discriminator despite being in the CLI’s docs — the app’s own pooled CLI processes create it too after the first app session (verified live 2026-08-31; the directory held exactly the app’s own session ids), so data.db alone discriminates.
Event vocabulary: reuses COPILOT_SDK_TOOL_MAP directly (src/platforms/copilot-app-adapter.ts imports it) — the pooled CLI is the same runtime, so the canonicalized names documented in the copilot-sdk section apply unchanged. Two app-level tool names with no Claude equivalent came through live: rename_session and rename_branch — deliberately left unmapped, falling through to 'Unknown' with the original name preserved.
Session correlation: the session_id in the hook events is byte-identical to sessions.id in the app’s ~/.copilot/data.db — hook-side actions and DB-side economics join on session id directly, with no correlation key or mapping table needed.
Token-exact cost: the .mjs SDK-extension path the copilot-sdk section documents is unavailable on this host — the Rust app has no extensions/ loading. Instead, data.db (SQLite, WAL mode, schema v100, held open by the app) carries the economics: its sessions table has cumulative per-session total_input_tokens/total_output_tokens/total_cached_tokens/total_reasoning_tokens, plus total_nano_aiu (GitHub’s billing meter, AIU × 1e9 — its exact semantics are not yet established, so Preflight does not read it yet), model (which can be the literal string auto), execution_location (local vs cloud sandbox), and ISO-8601 created_at/updated_at. There is no tool-call/event table — the hooks give actions with zero economics, the DB gives economics with zero actions; the shared session id joins them. CopilotAppUsageWatcher (src/hooks/copilot-app-usage-watcher.ts) polls data.db read-only and emits the same mode: 'token' buffer lines CopilotUsageWatcher emits for VS Code, inheriting that pipeline’s cache-inclusive-input assumption: total_input_tokens is presumed to include total_cached_tokens (as VS Code’s inputTokens verifiably does — see the comment above inputTokens in src/hooks/copilot-usage-watcher.ts), so cached tokens are billed once at the cache-read rate and only the remainder at the base input rate. That assumption is inherited, not independently verified against the app’s DB. Opt out with NR_AI_ENABLE_COPILOT_APP_USAGE_WATCHER=0.
Known gaps:
- All empirical claims are macOS-only. Windows/Linux install paths and directory layout are unverified.
- Cloud-sandbox sessions are unconfirmed. A session with
execution_location != 'local'runs where local hooks cannot fire; whether its economics still appear in the localdata.dbis unconfirmed. - Session end is never observed. The collector has no
Stop/SessionEndbranch for this platform, so a session’s outcome stays “in progress”. - MCP-server propagation to the pooled CLI processes is unconfirmed. The app spawned its CLI pool with
observed_process_mcp_config=0, so whether servers registered viacopilot mcp addreach those processes — and therefore whether thenr_observe_*tools are callable inside app sessions — is unconfirmed. - Concurrent multi-session capture is untested. Multiple simultaneous app sessions on one machine have not been exercised.
Setup (mirroring CopilotAppAdapter.getHookInstallInstructions()):
- Create
~/.copilot/hooks/preflight.json:The{"version": 1,"hooks": {"PreToolUse": [{ "type": "command", "command": "MCP_CLIENT=copilot-app preflight-collector pre-tool" }],"PostToolUse": [{ "type": "command", "command": "MCP_CLIENT=copilot-app preflight-collector post-tool" }]}}MCP_CLIENT=copilot-appstamp on each command is what routes the events to this adapter rather thancopilot-sdk. - Ensure
preflight-collectoris onPATH(npm link, ornpm install -g @newrelic/preflight) — the app resolves hook commands through your login shell, soPATHentries only visible in interactive shell startup files may not apply. - Register the Preflight MCP server:
Whether this registration reaches the app’s pooled CLI processes is unconfirmed (see Known gaps).
Terminal window copilot mcp add preflight \--env MCP_CLIENT=copilot-app \--env NEW_RELIC_LICENSE_KEY=<your-key> \--env NEW_RELIC_ACCOUNT_ID=<your-account-id> \-- npx preflight --stdio - There is no extension step — the Rust app cannot load the
copilot-sdk.mjsextension. Token-exact cost comes from the app’s own~/.copilot/data.db, read automatically byCopilotAppUsageWatcherinside the Preflight MCP/dashboard process.
Generic MCP fallback (generic-mcp)
Mechanism: Self-report via MCP tools. Always registered last and always isSupported() === true — the catch-all for any MCP-speaking client not otherwise named above.
Tools exposed:
nr_observe_report_tool_call— report a non-MCP tool call (file read/write/terminal command) manuallynr_observe_report_session_start/nr_observe_report_session_end— session lifecycle
Setup:
- Add the Preflight MCP server to your client’s MCP configuration:
npx preflight --stdio - Set
NEW_RELIC_LICENSE_KEY,NEW_RELIC_ACCOUNT_ID - MCP tool calls are captured automatically via the proxy
- Use
nr_observe_report_tool_callfor non-MCP tool activity - Use
nr_observe_report_session_start/nr_observe_report_session_endfor session tracking
Adding a new adapter
- Implement
PlatformAdapter(src/platforms/types.ts) in a newsrc/platforms/<name>-adapter.ts, including avisibilityLevel(full-hooks,self-reported, ormcp-tools-only— see the table above) —platform-registry.test.tsenforces every registered adapter declares one. - Source the tool-name map from the platform’s own documentation — never invent entries. Every existing adapter’s tool-map comment cites its source; do the same.
- If the platform has a real hook/callback mechanism, add its event vocabulary as new branches in
src/hooks/collector-script.ts(see Cursor’s or Windsurf’s branches for the pattern) — don’t assume the platform matches Claude Code’stool_name/tool_inputshape. - Register the adapter in
createDefaultRegistry()(src/platforms/platform-registry.ts), before the generic MCP fallback. - Add this platform’s section to this document, following the structure above: mechanism, detection env vars, tool-map/event vocabulary, known gaps, setup steps.
Footnotes
-
Pi has no MCP client support at all, by its own explicit design choice — unlike every other
full-hooksplatform in this row, its setup does not register an MCP server. See the Pi section below for its--local-mode-only setup path. ↩ -
VS Code Copilot sends the uniform
hook_event_name/tool_name/tool_input/session_idenvelope but with VS Code’s own tool names and camelCasetool_inputkeys — both deltas documented in the hooks FAQ (code.visualstudio.com/docs/copilot/customization/hooks). Copilot also retains a legacy HTTP-push fallback path (see its section below). ↩ -
Gemini CLI’s hook event names (
BeforeTool/AfterTool) don’t matchPreToolUse/PostToolUse, so it needs its own branches incollector-script.ts— but the fields inside those events (tool_name,tool_input,tool_response) are shaped exactly like Claude Code’s, so those branches reuse the existing field-extraction helpers rather than needing new ones. ↩ -
Antigravity’s hook payloads have no field whose value names the event (unlike every other row in this table) —
collector-script.tsdispatches on payload shape instead (presence of atoolCallkey meansPreToolUse). See the Antigravity section below. ↩