@ev-ai/agent-hook
v1.0.9
Published
Cross-runner runtime telemetry hook for Claude Code, Cursor, and Codex
Keywords
Readme
@ev-ai/agent-hook
Cross-runner runtime telemetry hook for Claude Code, Cursor, and OpenAI Codex. Each session (and selected tool/MCP events) POSTs a runtime envelope to your ev-ai collector at POST /{orgToken}/hook.
What this is (and isn't)
This is an opt-in, first-party observability agent that an ev-ai customer installs on their own developers' machines / CI to get an inventory and runtime view of their own org's AI coding-agent usage. It is the AI-coding-agent equivalent of an EDR/telemetry sensor or an APM agent: the org deploys it, on its own hosts, to send data to its own collector.
- Data goes to the customer's own collector. The destination is the org's ev-ai endpoint (default
https://hooks.ev-ai.ai/{orgToken}/hook, or a self-hosted collector via--url). It is not a third party from the org's perspective — the customer is the data controller. - It is consent-based and removable. Nothing installs silently: the operator enables the ev-ai plugins (or runs file-wiring
install), and uninstall / plugin disable removes ev-ai wiring. Non-managed hooks can be removed locally by the developer at any time. - Collection is deliberately minimized.
hook_inputdefaults to an allowlist of safe metadata (session/tool ids, status) — tool arguments, prompts, file contents, and shell commands are dropped unless the operator explicitly opts into full capture.env_varsis a fixed allowlist of non-secret CI/runtime markers; API keys and proxy vars are never read. See Privacy defaults. - What it is not: it is not spyware, not a keylogger, and not a data broker. It does not collect keystrokes, file contents, or credentials, and it does not exfiltrate to any endpoint the operator did not configure.
Install
Primary path: enable the ev-ai runner plugins (Claude / Cursor / Codex) from the hosted marketplace. Their hooks pin npx -y @ev-ai/agent-hook@<ver> --runner …. Your repo commits only a shared collector_url manifest plus plugin enablement — not per-event blocks in .claude/settings.json / .cursor/hooks.json / .codex/hooks.json.
Fallback: the install CLI merges hooks into those three config files directly.
Primary — plugin + npm
- Write the org destination (manifest only):
npx @ev-ai/[email protected] configure \
--url https://hooks.ev-ai.ai/<your-org-path-id>/hook--url must be the full ingest URL ending in /{orgToken}/hook. Prefer a long opaque hex org path id (64 lowercase hex chars). Safe to commit in private repos — auth is in the URL path, not a Bearer header.
- Enable the Claude / Cursor / Codex plugins from the hosted marketplace.
- Commit the shared manifest (
.claude/ev-ai-runtime-report.json) plus plugin enablement or the file-wired hook configs (not both for the same events). Restart runners.
Plugins pin an exact package version, never @latest.
Migrating from file-wiring: strip ev-ai per-event blocks while keeping the manifest:
npx @ev-ai/[email protected] migrate-to-pluginThen enable plugins. Do not leave both plugin and file hooks active — that double-POSTs.
Codex: plugin hooks stay inactive until trusted (/hooks in the CLI) or deployed as managed hooks.
Windows: not supported in v1. Use macOS/Linux.
Fallback — file wiring (install)
One-time wiring by the repo owner. Teammates pull committed hook configs and need Node only.
npx @ev-ai/agent-hook@latest install \
--url https://hooks.ev-ai.ai/<your-org-path-id>/hookWhen Claude Code already has native OTEL enabled (managed/user/project settings), install skips the Claude hook to avoid duplicate events. Cursor and Codex are still wired. Override with --force:
npx @ev-ai/agent-hook@latest install \
--url https://hooks.ev-ai.ai/<token>/hook \
--forceSkip a vendor entirely with --no-claude, --no-cursor, or --no-codex:
npx @ev-ai/agent-hook@latest install \
--url https://hooks.ev-ai.ai/<token>/hook \
--no-cursor --no-codexKeep Claude and Cursor both wired. Cursor also loads .claude/settings.json as third-party hooks; the binary suppresses those Claude/Codex-tagged twin invocations when it detects a Cursor host, so each event posts once as platform: cursor. Real Claude Code sessions still use the Claude hooks unchanged. When the hook process itself is inside Cursor's command sandbox (CURSOR_SANDBOX=seatbelt on macOS, native on Linux), it acks the runner and does not POST — so a nested shell cannot mint a second installation_id. Official Cursor session/tool hooks still fire from the IDE host, where that variable is unset. The sandbox boolean on beforeShellExecution stdin is different: that is whether the upcoming command will be sandboxed, and is still collected.
Stacked configs for the same runner (user-level ~/.cursor/hooks.json plus project .cursor/hooks.json, or plugins enabled alongside file wiring) also fire twice. The hook collapses those to one collect + POST via a short-lived cross-process claim under ~/.ev-ai/agent-hook/claims/ (keyed by platform, session, event, turn / tool_use_id, and a local hash of call-identifying stdin when tool_use_id is absent — so distinct Cursor shell/read/edit calls in one turn are not treated as duplicates). Weak payloads without a strong id fail open (both may post). Disable by setting EVENT_CLAIM_DEDUP_ENABLED to false in source — needed if two configs for the same runner intentionally target different collector URLs (the claim key does not include the URL, so only one would deliver).
Wired command shape (auth is in the committed collector_url). The per-event command is pinned to the installer's version (not @latest):
npx -y @ev-ai/[email protected] --runner cursorYou can optionally put the collector URL on the hook command itself — --url (or --token + optional --base-url) overrides any shared manifest, so .claude/ev-ai-runtime-report.json is not required:
{
"command": "npx -y @ev-ai/[email protected] --runner claude --url 'https://hooks.ev-ai.ai/<orgToken>/hook'"
}Commit .claude/ev-ai-runtime-report.json (contains collector_url) plus .claude/settings.json, .cursor/hooks.json, and .codex/hooks.json when using this path (or omit the manifest if every wired command already has --url). Do not also enable the ev-ai plugins for the same events — that double-posts.
Install is additive: existing non-ev-ai hooks are kept; re-install replaces only ev-ai-owned entries. Uninstall removes only ev-ai entries (and the ev-ai manifest).
Alternative — CDN binary (no Node at hook time)
For environments where Node is not on PATH when runners fire hooks (typically with file-wiring or a future plugin that bundles a binary):
curl -fsSL https://cdn.ev-ai.ai/agent-hook/install.sh | sh -s -- -- \
--url https://hooks.ev-ai.ai/<token>/hookPin a specific release:
AGENT_HOOK_VERSION=1.0.6 curl -fsSL https://cdn.ev-ai.ai/agent-hook/install.sh | sh -s -- -- \
--url https://hooks.ev-ai.ai/<token>/hookThe installer downloads a standalone binary (~50–100MB) for your OS/arch, verifies SHA256, caches it under ~/.ev-ai/agent-hook/<version>/, copies it into the git repo at .ev-ai/agent-hook/ev-ai-agent-hook, and wires all three runners with --delivery binary (default for the compiled installer).
Wired command shape (git-root-relative path + --runner):
"$(git rev-parse --show-toplevel)/.ev-ai/agent-hook/ev-ai-agent-hook" --runner cursorInstall per machine for the binary path: each collaborator runs install once to download their platform binary into .ev-ai/agent-hook/ (gitignored).
Force npx delivery from Node even when a global binary pin exists:
npx @ev-ai/agent-hook@latest install \
--url https://hooks.ev-ai.ai/<token>/hook \
--delivery npxIdentity and runtime notes
Each developer's installation_id is created on first hook fire and stored once per machine under ~/.ev-ai/agent-hook/installation.json (shared across all repos on that machine). Ephemeral CI/cloud homes (no durable ~/.ev-ai) derive a stable id from platform seeds (e.g. GitHub Actions GITHUB_REPOSITORY_ID + GITHUB_ACTOR, Codespaces CODESPACE_NAME, Cursor Cloud CURSOR_PROJECT_DIR + CURSOR_USER_EMAIL, Claude Code remote origin remote + git author email) so collector rows do not churn every run. Cursor Cloud / Claude remote snapshot images that bake a random installation.json are rewritten to that derived id. Pin explicitly with EV_AI_HOOK_INSTALLATION_ID (8–64 chars [A-Za-z0-9._-]) when the environment has no actor seed.
Per-event execution uses npx (primary) or the pinned binary (CDN path). The runner-facing process acks immediately ({"continue":true} + exit 0) after draining stdin and spawning a detached __work child that collects + POSTs — except teardown events, which stay in-process and await the HTTP POST so a runner that tears down the process tree on exit cannot kill the worker mid-flight. Teardown = Claude SessionEnd / StopFailure, Codex Stop (its only end signal), and SubagentStop. Per-turn Claude Stop / Cursor stop fire at the end of every turn (not teardown), so they detach like tool events and never block the runner between turns. Set EV_AI_HOOK_SYNC=1 to keep all events in-process for debugging.
Gitignore (recommended)
/.ev-ai/Do not gitignore .claude/ev-ai-runtime-report.json when using a committed org path id — teammates need it to post without env vars. Keep the repo private; rotate the path id via your org settings if it leaks.
For the plugin path, do not add ev-ai per-event hooks to .claude/settings.json / .cursor/hooks.json / .codex/hooks.json. For the file-wiring fallback, add those files only if your repo does not already track other hook settings you need to share.
Fail-open contract
- Always prints
{"continue":true}to stdout and exits0 - High-frequency and per-turn events (incl. Claude
Stop/ Cursorstop): collect + POST in a detached__workchild so the runner is not blocked - Teardown events stay in-process (sync POST) — Claude
SessionEnd/StopFailure, CodexStop, andSubagentStop, because the runner may kill detached children when the session/worker tree is torn down - POST / spawn failures log to stderr only
Debug envelope logging: EV_AI_HOOK_DEBUG=1 (redacted summary). Full envelope: EV_AI_HOOK_DEBUG=full. Force all events in-process: EV_AI_HOOK_SYNC=1. Disable duplicate-config claim collapsing by setting EVENT_CLAIM_DEDUP_ENABLED to false in source (use when two configs for the same runner must POST to different collectors).
Privacy defaults
Full field-by-field inventory of what is POSTed: COLLECTED-DATA.md.
hook_input: allowlisted metadata only (session_id,tool_name,status, …). Tool args / prompts / session titles are dropped. Opt-in full stdin: untracked operator manifestcapture_hook_input: "full"orEV_AI_HOOK_CAPTURE_FULL_INPUT=1(still size-capped). A git-tracked (committed) manifest cannot setfull— use the env var or an untracked operator manifest instead.env_vars: fixed allowlist only (CI markers,GITHUB_*context, vendor base-URL overrides). Gateway URL values (*_BASE_URL, etc.) are sent with host/path and non-sensitive query keys for proxy-substitution detection; embedded URL credentials (user:pass@) and credential-shaped query params (key,api-key,token,sig, …) are redacted. API keys and HTTP proxy vars are never read.- Transport: collector URL must be
https(orhttp://localhost/127.0.0.1, which is allowed only so a collector can be exercised on the same machine). Cleartext remote URLs are blocked (fail-open, no POST).
Uninstall
Plugin path: disable / uninstall the ev-ai marketplace plugins, then remove or rotate the shared manifest.
File-wiring → plugin: keep the manifest, strip only ev-ai file hooks:
npx @ev-ai/agent-hook migrate-to-pluginFile-wiring full uninstall (also deletes the shared manifest):
npx @ev-ai/agent-hook uninstallOr via the CDN-installed binary:
"$HOME/.ev-ai/agent-hook/current/ev-ai-agent-hook" uninstallOr:
curl -fsSL https://cdn.ev-ai.ai/agent-hook/install.sh | sh -s -- uninstalluninstall removes ev-ai-owned hook entries from Claude/Cursor/Codex config files (leaves other hooks intact) and deletes ev-ai-runtime-report.json manifest(s) in the current git repo. Does not remove ~/.ev-ai/agent-hook/ (pinned binary) or ~/.ev-ai/agent-hook/ (per-machine identity).
