npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ev-ai/agent-hook

v1.2.8

Published

Cross-runner runtime telemetry hook for Claude Code, Cursor, and Codex

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 opt-out for prompt / tool-arg / tool-result / last-assistant-message bodies. Those four hook_input fields are sent by default (size-capped). Set EV_AI_HOOK_CAPTURE_CONTENT=off or capture_hook_input: "allowlist" to drop them. On Claude that includes file bodies written or read through a tool, which arrive nested in tool_input / tool_response. Session titles, chat transcripts, keystrokes, and runner error prose stay dropped. env_vars is 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, does not scrape the filesystem, and does not exfiltrate to any endpoint the operator did not configure. It does forward the bodies of tool calls the agent makes, unless content capture is turned off.

What this package does on your machine

A plain list of every behavior a security reviewer or supply-chain scanner will want to know about. Details and opt-outs are in the linked sections.

At install time: nothing. The package has no preinstall / install / postinstall scripts, no native modules, and no runtime dependencies. Code runs only when a runner (Claude Code, Cursor, Codex) fires a configured hook, or when you run the CLI yourself.

Network requests (all HTTPS, except http:// to loopback for a local collector):

  • POST {collector_url} on hook events — the runtime envelope described in COLLECTED-DATA.md. The destination is only the collector URL you configure (--url, the committed ev-ai-runtime-report.json, or ~/.ev-ai/config.json).
  • GET /{orgToken}/version on that same collector, and — when the collector returns a signed envelope — a download of hook.mjs from the signed URL — the auto-update path. See Hook updates.
  • Cloud instance metadata, only in non-interactive contexts (CI / cloud VMs, never a developer's interactive session), at most once per 24 h: AWS IMDSv2 (169.254.169.254), GCP (metadata.google.internal), Azure (169.254.169.254). Only provider, region and instance id are read; no credentials or tokens are requested beyond the short-lived IMDSv2 session token that AWS requires for the read.

Auto-update: the package can run code it did not ship with. On the npx path the launcher may run a newer hook.mjs that your collector selects, downloaded to ~/.ev-ai/agent-hook/js/signed-v1/, verified against an ev-ai signed envelope (built-in public key) and SHA-512 of the bytes, and executed with the same Node that runs the launcher. It is on by default; turn it off with EV_AI_AUTO_UPDATE=0 or "auto_update": false in ~/.ev-ai/config.json, and the pinned, bundled hook is the only code that runs. A repo manifest cannot change this setting.

Background processes. To ack the runner quickly, most events are collected by a detached child process that exits after one POST. At session start the launcher may also spawn one detached __refresh process that performs the update check above, then collects that SessionStart with the version the check selected, and exits (skipped when auto-update is off). While it is held for the check, the SessionStart's payload is kept in a handoff-*.json file (mode 0600) in the collector's scope directory under ~/.ev-ai/agent-hook/js/signed-v1/scopes/, and deleted as soon as the refresh reads it.

Files read (identity and configuration only, never file contents from your projects):

  • Runner configs: .claude/, .cursor/, .codex/ hook / plugin / MCP settings at user, project and managed scope. MCP env blocks are reduced to variable names; values are never read into the envelope.
  • Operator identity: ~/.claude.json (the oauthAccount login email and name), ~/.cursor/cli-config.json, Cursor's state.vscdb (only cursorAuth/* rows, with every key containing token excluded in the SQL query and again in code), and ~/.codex/auth.json (only the ID-token's identity claims are decoded locally; the API key and access / refresh tokens are never read out or sent).
  • Git: remote.origin.url, user.email, last commit author, repo root.
  • A fixed allowlist of non-secret environment variables (CI / runtime markers). API keys, tokens and proxy variables are never read.

Files written. Only under ~/.ev-ai/ (installation id, caches, update artifacts, short-lived dedup claims) and, when you run configure / install, the runner hook config files and manifest in the repo. uninstall removes the ev-ai entries.

Subprocesses. git, and for OS / host detection sw_vers, uname, lsb_release, tty, systemd-detect-virt; sqlite3 as a fallback reader for Cursor's state.vscdb. All are invoked with fixed argument lists (no shell) and short timeouts.

Content capture. Prompt, tool-argument, tool-result and last-assistant-message bodies are sent by default (size-capped). Turn them off with EV_AI_HOOK_CAPTURE_CONTENT=off. See Privacy defaults.

Shipped code is bundled and minified (dist/*.mjs, built with esbuild from TypeScript).

Install

Primary path: enable the ev-ai runner plugins (Claude / Cursor / Codex) from the hosted marketplace. Their hooks run the shipped hook.mjs with Node (no package version in the command). 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

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

  1. Enable the Claude / Cursor / Codex plugins from the hosted marketplace.
  2. 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.

File-wiring pins an exact package version, never @latest. Plugin runtime bumps ride the shipped hook.mjs payload.

Migrating from file-wiring: strip ev-ai per-event blocks while keeping the manifest:

npx @ev-ai/[email protected] migrate-to-plugin

Then 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>/hook

When 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 \
  --force

Skip 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-codex

Keep 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 --prefer-offline @ev-ai/[email protected] --runner cursor

You 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 --prefer-offline @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. Fire-time lookup: EV_AI_HOOK_MANIFEST if set (exclusive), else ~/.ev-ai/config.json merged with .claude/ / .cursor/ / .codex/ ev-ai-runtime-report.json under the git root / cwd — home keys win; the repo file fills in missing keys.

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

Pin 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>/hook

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

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

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

Hook updates (npx path)

The npx package's entry point is a small launcher. It runs the hook your collector selects, from a per-machine cache under ~/.ev-ai/agent-hook/js/signed-v1/, and otherwise the hook bundled in the pinned package. A cached hook is re-verified (signature + SHA-512) on every event, and any problem with it (missing, altered, unsigned, incompatible with this Node or launcher, or failing to start twice in a row) falls back to the bundled hook for that event.

Network. Besides the collector POST, the launcher makes two kinds of request, both only on the npx path:

  • GET /{orgToken}/version on the same collector, at most once per 6 h per collector: on the first event in a new collector scope, then from a detached background refresh at session start. No request is made on other events.
  • When the collector returns a signed envelope, a download of that release's hook.mjs from the signed URL (https://…/{version}/hook.mjs). The launcher verifies the envelope with a built-in public key, then checks the bytes against the signed SHA-512 before they are written to the cache. The launcher never downloads from npm and does not hardcode a CDN hostname.

Kill switch. EV_AI_AUTO_UPDATE=0, or "auto_update": false in ~/.ev-ai/config.json, always runs the bundled hook and makes neither request. Ephemeral CI homes start cold on every run, so their first event makes the /version check (within a 3 s budget); set EV_AI_AUTO_UPDATE=0 in CI if you want the pinned hook with no extra request.

Status. npx @ev-ai/[email protected] status prints which hook runs for the current repo's collector and why, when the next check may happen, and the last check's result. --all adds every other collector scope on the machine, and --json prints the same fields as JSON. status never makes a request.

Cache. Deleting ~/.ev-ai/agent-hook/js/ is safe at any time: the next event runs the bundled hook and checks again.

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 exits 0
  • High-frequency and per-turn events (incl. Claude Stop / Cursor stop): collect + POST in a detached __work child so the runner is not blocked
  • Teardown events stay in-process (sync POST) — Claude SessionEnd / StopFailure, Codex Stop, and SubagentStop, 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: metadata (session/tool ids, status, …) plus content fields by default — prompt, tool_input, tool_response, last_assistant_message, and Cursor command. Kill switch: EV_AI_HOOK_CAPTURE_CONTENT=off or untracked/tracked manifest capture_hook_input: "allowlist". Session titles, transcript paths, runner error prose, and file/attachment paths stay dropped, as do the MCP spawn argv on beforeMCPExecution and the error prose StopFailure puts in last_assistant_message. Full stdin dump: untracked operator manifest capture_hook_input: "full", debug: true on ~/.ev-ai/config.json, or EV_AI_HOOK_CAPTURE_FULL_INPUT=1 (still size-capped). debug does not change the collector URL. A git-tracked (committed) manifest cannot set full or debug — use the env var or an untracked operator / home overlay 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 (or http://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).
  • User spec / project directories. Accepting home-directory inventory (~/.claude, ~/.cursor, ~/.codex) does not by itself describe the project walks. Those also inventory gitignored agent config in the working tree (settings.local.json, CLAUDE.local.md, uncommitted edits) and every directory a session opens — including a personal clone on a work machine (cwd walk always; repo walk when that clone has an origin remote) and a covering parent above the git root (for example ~/dev/CLAUDE.md sitting above both work and personal clones). Disable the project walks with EV_AI_HOOK_CWD_SPEC=off. Disable home spec, project spec, and operator identity together with EV_AI_HOOK_USER_SPEC=off.

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

File-wiring full uninstall (also deletes the shared manifest):

npx @ev-ai/agent-hook uninstall

Or via the CDN-installed binary:

"$HOME/.ev-ai/agent-hook/current/ev-ai-agent-hook" uninstall

Or:

curl -fsSL https://cdn.ev-ai.ai/agent-hook/install.sh | sh -s -- uninstall

uninstall 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, per-machine identity, and the npx hook cache under ~/.ev-ai/agent-hook/js/).