@ev-ai/agent-hook 1.0.6 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COLLECTED-DATA.md +21 -13
- package/README.md +50 -57
- package/dist/cli.mjs +25 -16
- package/dist/hook.mjs +21 -11
- package/dist/plugin-hooks.mjs +11 -6
- package/package.json +4 -26
package/COLLECTED-DATA.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Collected data
|
|
2
2
|
|
|
3
|
-
What `@
|
|
3
|
+
What `@ev-ai/agent-hook` POSTs to the ev-ai collector (`POST /{orgToken}/hook`, default `https://hooks.ev-ai.ai/{orgToken}/hook`). Auth is the org path id in the URL — no Bearer header and not in the JSON body. See [README.md](./README.md) for install and privacy defaults.
|
|
4
4
|
|
|
5
5
|
## Tracked events
|
|
6
6
|
|
|
@@ -10,17 +10,20 @@ What `@evalon-ai/agent-hook` POSTs to the Evalon collector (`POST /{orgToken}/ho
|
|
|
10
10
|
| `session_end` | Session stops |
|
|
11
11
|
| `tool_invoked` | Tool, skill, MCP, shell, or file-edit hook fires |
|
|
12
12
|
|
|
13
|
-
Every event also carries: UTC timestamp, `installation_id
|
|
13
|
+
Every event also carries: UTC timestamp, `installation_id` (per-machine under `~/.ev-ai/agent-hook/installation.json`; on ephemeral CI/cloud homes derived from platform seeds such as `GITHUB_REPOSITORY_ID`+`GITHUB_ACTOR` / `CODESPACE_NAME` / Cursor Cloud `CURSOR_PROJECT_DIR`+`CURSOR_USER_EMAIL` / Claude Code remote origin remote + git author email, or pinned via `EV_AI_HOOK_INSTALLATION_ID`), runner `platform` (`claude` / `cursor` / `codex`), `platform_agent_id` (Layer-1 inventory join key: `{owner}/{repo}:{scopePath/}{markerDir}`, e.g. `acme/my-app:.claude` or `acme/my-app:apps/web/.cursor`), `session_id`, `turn_id`, raw `runner_event` name, `agent_hook_version`, `host_arch`, `node_version`, and `build_ms` (wall-clock milliseconds to assemble the envelope before the collector POST; not tool/session duration).
|
|
14
14
|
|
|
15
15
|
## Tracked data points
|
|
16
16
|
|
|
17
17
|
### On `session_start`
|
|
18
18
|
|
|
19
|
-
- **Host** — hostname, OS / version, username, shell, TTY, runtime context (interactive / CI / container /
|
|
19
|
+
- **Host** — hostname, OS / version, username, shell, TTY, runtime context (`interactive` / `github_actions` / `claude_remote` / `cursor_cloud` / `codespaces` / `gitlab_ci` / `managed` / `container` / `unknown`). Cloud / CI contexts require positive evidence (`GITHUB_ACTIONS`, `CLAUDE_CODE_REMOTE`, `CODESPACES`, `GITLAB_CI`, managed CI platform markers, truthy `CI`, or Cursor Cloud). Cursor Cloud is `CURSOR_AGENT` without a local IDE host, a `bc-` background-composer session id on Linux, or hostname `cursor` + os user `ubuntu` + `CURSOR_VERSION` on Linux. `CURSOR_AGENT` / those extra markers are not enough when a local Cursor / VS Code extension host is present (desktop agent shells set `CURSOR_AGENT` too). `CURSOR_CODE_REMOTE` alone is not Cursor Cloud (Remote-SSH boxes also set it). `gitlab_ci` is `GITLAB_CI`. `managed` covers generic `CI=true` plus named platforms that often omit it (`TF_BUILD`, `JENKINS_URL`, `BUILDKITE`, `CIRCLECI`, `CODEBUILD_BUILD_ID`, `DRONE`, …). `container` covers `/.dockerenv`, k8s (`KUBERNETES_SERVICE_HOST`), and VS Code / Codespaces-style remote containers (`REMOTE_CONTAINERS` / `DEVCONTAINER`). `interactive` covers TTY / `TERM` / `TERM_PROGRAM`, SSH session markers, macOS GUI launch (`__CFBundleIdentifier`, `XPC_SERVICE_NAME`), IDE host markers (`VSCODE_PID`, …), and Linux / Windows desktop session vars. Residual with no positive cloud or local evidence is `unknown` (the former `cron` catch-all for “no TTY” was removed — GUI-launched agents often have no TTY). Optional virt and cloud provider / region / instance id. Cloud metadata is probed only for non-interactive contexts; interactive laptop sessions skip IMDS entirely. Non-interactive probes are cached locally for 24h per context.
|
|
20
20
|
- **Git** — origin remote (credentials stripped), branch, HEAD SHA, dirty flag
|
|
21
|
-
- **Env (allowlist only)** — CI markers (`CI`, `GITHUB_*`, `GITLAB_CI`, …), `NODE_ENV`, `TERM_PROGRAM`, vendor base-URL overrides (`ANTHROPIC_BASE_URL`, `OPENAI_BASE_URL`, …), Claude Bedrock/Vertex flags. API keys and proxy vars are never read. Gateway URLs have embedded credentials and credential-shaped query params (`key`, `api-key`, `token`, `sig`, …) redacted.
|
|
21
|
+
- **Env (allowlist only)** — CI markers (`CI`, `GITHUB_*`, `GITLAB_CI`, `CIRCLECI`, `BUILDKITE`, `TF_BUILD`, …), actor identity markers (`GITHUB_ACTOR`, `GITLAB_USER_*`, `BUILD_REQUESTEDFOR*`, `BUILDKITE_BUILD_CREATOR*`, `BUILD_USER*`, `CHANGE_AUTHOR*`, `CIRCLE_USERNAME`, `CODEBUILD_INITIATOR`, `DRONE_COMMIT_AUTHOR*`, `EV_AI_HOOK_OPERATOR_*`, `GIT_AUTHOR_*`, `GIT_COMMITTER_*`, `EMAIL`, …), cloud session markers (`CLAUDE_CODE_REMOTE`, `CURSOR_AGENT`, `CURSOR_VERSION`, `CURSOR_USER_EMAIL`, `CODESPACES`), `CURSOR_PROJECT_DIR` (present/absent only — the path is not shipped), `NODE_ENV`, `TERM_PROGRAM`, vendor base-URL overrides (`ANTHROPIC_BASE_URL`, `OPENAI_BASE_URL`, …), Claude Bedrock/Vertex flags. API keys and proxy vars are never read. Gateway URLs have embedded credentials and credential-shaped query params (`key`, `api-key`, `token`, `sig`, …) redacted.
|
|
22
22
|
- **MCP inventory** — declared server **names** (and discovery source), not commands/URLs/env
|
|
23
23
|
- **User-scope config** — `cli_version` (claude / agent|cursor-agent / codex `--version`); model (Claude; SessionStart stdin `model` preferred over settings-file), Claude-only `claude_version` mirror; presence/size of global settings / memory / MCP files; name+size listings of global skills, commands, subagents, rules (full listing when the hash changed or every 10th session start as a repair re-send; capped at 500 entries per list with `truncated: true` when capped). File **contents** are not sent.
|
|
24
|
+
- **User spec (home-dir components)** — per-component delta of skills / commands / rules / memory / subagents / hooks / MCP servers / plugins from `~/.claude`, `~/.cursor`, `~/.codex`, `~/.agents`. Project spec in the working directory is not part of this snapshot (in-repo files go through the git inventory pipeline; non-git cwd is recorded only as `hook_input.cwd`). Walked on the **first** `session_start` for a runner and again only after a **14-hour cooldown** (`user_spec_collected_at_ms` in `self-config-cache.json`); mid-cooldown session starts skip the home walk and omit `user_spec*` / `operator`. Each upsert row carries `kind`, path-derived `name`, home-relative `path`, capped file `text` (≤20_000 chars) for file-backed kinds, `content_hash`, `origin` (`user` or `plugin:<name>@<marketplace>`), `enabled`, optional `trigger` / `attributes`. Frontmatter fields (`name`, `description`, …) are not parsed on the client — the collector derives them from `text`. Only **changed or new** components are sent (`user_spec`); removals are explicit tombstones (`user_spec_removed: [{ path, name }]`). Unchanged scope after a cooldown refresh → hash/count/truncated scalars alone. First walk is a full authoritative snapshot (`user_spec_full: true`). File `text` is shipped **once per `content_hash`** (later sends omit the body when that hash was already delivered); under the ~300 KiB user-spec byte budget, overflowing bodies are deferred (`user_spec_text_deferred`) before rows are dropped. Self-config / operator caches commit only after a successful collector POST. Hook rows send `action_kind` + `action_hash` only — never the command string. MCP rows send name + transport only. Plugin rows never include `installPath` / `projectPath`. Disabled with `EV_AI_HOOK_USER_SPEC=off`.
|
|
25
|
+
- **Operator identity** — on the first user-spec collect for an installation (and again when the 14-hour user-spec cooldown elapses): `git_user_name` / `git_user_email` from `git config` when set (`identity_source: "git_config"`). When unset, tries `GIT_AUTHOR_NAME`/`GIT_AUTHOR_EMAIL`, then `GIT_COMMITTER_*`, then `EMAIL` (`identity_source: "git_env"`). Bot-shaped git identity (`*[bot]*`, `action@github.com`, `noreply@anthropic.com`, `cursoragent@cursor.com`, …) is skipped so CI/cloud actor env vars can win: explicit `EV_AI_HOOK_OPERATOR_NAME` / `EV_AI_HOOK_OPERATOR_EMAIL` / `EV_AI_HOOK_OPERATOR_LOGIN` → `identity_source: "configured"`; then `GITHUB_ACTOR` / `GITHUB_TRIGGERING_ACTOR` / `GITHUB_USER` → `"github"`; `GITLAB_USER_*` → `"gitlab"`; Azure DevOps `BUILD_REQUESTEDFOR*` → `"azure_devops"`; Buildkite `BUILDKITE_BUILD_CREATOR*` → `"buildkite"`; Jenkins `BUILD_USER*` / `CHANGE_AUTHOR*` → `"jenkins"`; CircleCI `CIRCLE_USERNAME` → `"circleci"`; CodeBuild `CODEBUILD_INITIATOR` → `"codebuild"`; Drone/Woodpecker `DRONE_COMMIT_AUTHOR*` → `"drone"`. When those are also absent, falls back to the HEAD commit author from `git log -1` (`identity_source: "git_log"`). Claude Code remote (`CLAUDE_CODE_REMOTE`) and Cursor cloud (`CURSOR_AGENT`) without a git or CI actor are tagged `identity_source: "cloud"`; Cursor cloud sets `git_user_email` from `CURSOR_USER_EMAIL` when it is not the image-baked bot address (never a session uuid). CI/cloud actor sources also set `platform_login` (GitHub/GitLab/CircleCI/CodeBuild/Jenkins `BUILD_USER_ID`/Drone login, Azure DevOps `BUILD_REQUESTEDFORID`, Buildkite creator email, or `EV_AI_HOOK_OPERATOR_LOGIN`) so the collector can join installations to members without inventing a noreply email. Omitted while user-spec is on cooldown and in steady state when unchanged. Never invents an OS username. Disabled with the same `EV_AI_HOOK_USER_SPEC=off` opt-out.
|
|
26
|
+
- **Cloud identity bootstrap** — Cursor cloud agents do not fire `sessionStart`. On the first `tool_invoked` / skill event when operator identity has never been sent (any runtime context — cloud VMs with a pseudo-TTY often look `interactive`), the hook POSTs a lightweight `session_start` (`run_meta.trigger: cloud_identity_bootstrap`) with host/context/operator — no home user-spec walk, and no `cli_version` / `model` / `user_scope` / `hook_input` (those stay unset so the collector never treats bootstrap nulls as a probed empty config). Debounced in-process and across concurrent hook processes via an exclusive claim lock beside `installation.json` (`cloud-bootstrap.lock`); skipped when `EV_AI_HOOK_USER_SPEC=off`.
|
|
24
27
|
- **Permission keys (Claude)** — each rule as `{ key, mode }` where `mode` is the list it came from (`allow` / `deny` / `ask`), so a blocked rule is never mistaken for a permitted one. Absolute POSIX / Windows / `~`-anchored paths inside a rule are replaced with `<path>` (`Bash(find /Users/me/repo -name x)` → `Bash(find <path> -name x)`) — the rule shape is the posture signal, the operator's home layout is not. Capped at 200 rules, 200 characters each.
|
|
25
28
|
- **Allowlisted hook metadata** — see below (includes SessionStart `model`, `permission_mode`, …)
|
|
26
29
|
|
|
@@ -33,22 +36,25 @@ Every event also carries: UTC timestamp, `installation_id`, runner `platform` (`
|
|
|
33
36
|
### On `tool_invoked`
|
|
34
37
|
|
|
35
38
|
- `tool_name`, `skill_name`
|
|
36
|
-
- **Cursor `skill_name` (derived, not from a native Skill tool):** slash commands via `beforeSubmitPrompt` (`/my-skill` → `tool_name: Skill`, `skill_name: my-skill`; prompt text is never POSTed); Read of `**/skills/<id>/SKILL.md` on `preToolUse` / `postToolUse` (skill id only — file path is never POSTed). Silent auto-inject with no slash and no SKILL.md Read is not observable.
|
|
39
|
+
- **Cursor `skill_name` (derived, not from a native Skill tool):** slash commands via `beforeSubmitPrompt` (`/my-skill` → `tool_name: Skill`, `skill_name: my-skill`; prompt text is never POSTed); Read of `**/skills/<id>/SKILL.md` on `preToolUse` / `postToolUse` / `beforeReadFile` (skill id only — file path is never POSTed); Shell whose command references a skill path on `beforeShellExecution` / `afterShellExecution` (id only — command string never POSTed). Silent auto-inject with no slash and no SKILL.md Read is not observable.
|
|
37
40
|
- **Claude `skill_name` (derived):** slash commands via `UserPromptExpansion` (`command_name` → `tool_name: Skill`, `skill_name: <id>`; `prompt` / `command_args` are never POSTed). Typing `/skillname` bypasses `PreToolUse(Skill)`; Read of `**/skills/<id>/SKILL.md` on Pre/Post tool hooks is also derived when present.
|
|
38
41
|
- **Codex `skill_name` (derived):** slash / custom-prompt ids via `UserPromptSubmit` (`/my-skill` → `tool_name: Skill`, `skill_name: my-skill`; prompt text is never POSTed); Bash/Shell whose `tool_input.command` references `.claude/.cursor/.agents/skills/<id>/SKILL.md` or `.codex/commands/<id>.md` (id only — command string never POSTed on allowlist); Read-like tools of those paths when present. Silent auto-inject with no slash and no skill-file touch is not observable.
|
|
39
42
|
- `tool_use_id` — opaque Claude Pre/Post join key when present in stdin
|
|
40
|
-
- `success` — `true` on Claude/Cursor `PostToolUse` / `postToolUse`, `false` on `PostToolUseFailure` / `postToolUseFailure` / Claude `PermissionDenied`, otherwise `null`
|
|
43
|
+
- `success` — `true` on Claude/Cursor `PostToolUse` / `postToolUse`, `false` on `PostToolUseFailure` / `postToolUseFailure` / Claude `PermissionDenied`, otherwise `null` (Cursor `afterFileEdit` / `afterShellExecution` have no exit code; Post* owns outcome)
|
|
41
44
|
- `failure_reason` — when `success === false`, a **failure class, never a runner message**: `interrupted` (stdin `is_interrupt`), `permission_denied` (Claude `PermissionDenied`), an identifier-shaped `failure_type` / `error_type` (`timeout`, `ShellError`), else the fixed value `error`. Runner `error` / `error_message` strings are dropped — they carry raw tool stderr including absolute paths and whole result bodies.
|
|
42
|
-
- `duration_ms` — from Claude `duration_ms` or Cursor `duration` on post-tool stdin
|
|
45
|
+
- `duration_ms` — from Claude `duration_ms` or Cursor `duration` on post-tool / `afterShellExecution` stdin
|
|
43
46
|
- `mcp_server` / `mcp_tool` — derived from opaque `tool_name` when it matches Claude `mcp__<server>__<tool>` or Cursor `MCP:<server>:<tool>` / `MCP:<tool>` (no `tool_input` parsing)
|
|
47
|
+
- `component_reference` — an opaque exact-component identifier emitted only when the hook has complete repository-backed identity evidence. Exact skill paths map to SKILL installations; exact MCP server and MCP tool invocations map to the server INTERFACE installation, never to a tool installation. Name-only, flat/unattributed MCP, malformed scope, and missing platform-agent identity stay unmatched. Absolute local paths never leave the hook process: path evidence is normalized to the repository-relative source path before hashing, and only the opaque reference is sent.
|
|
44
48
|
- `subagent_type` — on subagent start/stop hooks, which carry an agent type instead of a `tool_name`: Claude `agent_type` (`Explore`, plugin-scoped `my-plugin:reviewer`) or Cursor `subagent_type` (`generalPurpose`, `explore`). Only identifier-shaped values are sent; the neighbouring `task` / `description` / `summary` fields are free prose and are never captured
|
|
45
49
|
- Allowlisted hook metadata (see below)
|
|
46
50
|
|
|
47
51
|
### Hook metadata (default)
|
|
48
52
|
|
|
49
|
-
From runner stdin, only: session/conversation/turn ids (`prompt_id` / `prompt.id` / `generation_id` / `turn_id`), event name/source, status / `stop_reason` / SessionEnd `reason`, tool/skill name, `tool_use_id`, `permission_mode`, active `model` / Cursor `model_id`, `agent_id` / `agent_type`, Cursor `subagent_id` / `subagent_type` / `subagent_model` / `is_parallel_worker`, `effort` / `effort.level`, `duration_ms` / `duration`, failure category fields (`failure_type`, `is_interrupt` — the message fields `error`, `error_details`, `error_message` are **not** forwarded), Claude `UserPromptExpansion` metadata (`command_name`, `expansion_type`, `command_source` — never `prompt` / `command_args`), and git-root-relative `cwd_relative`
|
|
53
|
+
From runner stdin, only: session/conversation/turn ids (`prompt_id` / `prompt.id` / `generation_id` / `turn_id`), event name/source, status / `stop_reason` / SessionEnd `reason`, tool/skill name, `tool_use_id`, `permission_mode`, active `model` / Cursor `model_id`, `agent_id` / `agent_type`, Cursor `subagent_id` / `subagent_type` / `subagent_model` / `is_parallel_worker`, `effort` / `effort.level`, `duration_ms` / `duration`, Cursor shell `sandbox` (boolean), failure category fields (`failure_type`, `is_interrupt` — the message fields `error`, `error_details`, `error_message` are **not** forwarded), Claude `UserPromptExpansion` metadata (`command_name`, `expansion_type`, `command_source` — never `prompt` / `command_args`), absolute `cwd` when stdin has it, and git-root-relative `cwd_relative` in addition when the session is inside a git repo and stdin cwd is under that root. Session titles, tool args, prompts, shell commands, file paths, file contents, edits, MCP spawn commands/URLs, and tool result bodies are dropped unless the operator opts into full capture (`capture_hook_input: "full"` or `EV_AI_HOOK_CAPTURE_FULL_INPUT=1`).
|
|
50
54
|
|
|
51
|
-
Install wires Claude `SessionStart` / `UserPromptExpansion` / `PreToolUse` (all tools) / `PostToolUse` / `PostToolUseFailure` / `PermissionDenied` / `Stop` / `StopFailure` / `SessionEnd` / `SubagentStart` / `SubagentStop`; Cursor `sessionStart` / `beforeSubmitPrompt` / `preToolUse` / `postToolUse` / `postToolUseFailure` / `subagentStart` / `subagentStop` / `stop`; Codex `SessionStart` / `UserPromptSubmit` / `PreToolUse` (all tools) / `PostToolUse` / `Stop`. Re-run `install` to upgrade existing repos (Cursor
|
|
55
|
+
Install wires Claude `SessionStart` / `UserPromptExpansion` / `PreToolUse` (all tools) / `PostToolUse` / `PostToolUseFailure` / `PermissionDenied` / `Stop` / `StopFailure` / `SessionEnd` / `SubagentStart` / `SubagentStop`; Cursor `sessionStart` / `beforeSubmitPrompt` / `preToolUse` / `postToolUse` / `postToolUseFailure` / `beforeShellExecution` / `afterShellExecution` / `beforeReadFile` / `afterFileEdit` / `beforeMCPExecution` / `subagentStart` / `subagentStop` / `stop`; Codex `SessionStart` / `UserPromptSubmit` / `PreToolUse` (all tools) / `PostToolUse` / `Stop`. Re-run `install` to upgrade existing repos (Cursor-specific shell/read/edit/MCP events are added beside generic pre/post tool hooks; `runner_event` distinguishes the row).
|
|
56
|
+
|
|
57
|
+
When Cursor loads Claude (or Codex) file hooks as third-party configs alongside `.cursor/hooks.json`, the binary drops the foreign `--runner` twin so `platform` stays `cursor` and events are not duplicated. Claude Code and Codex hosts are unaffected. Stacked same-runner configs (user + project hooks, or plugin + file wiring) collapse to one POST via a short-lived claim lock (`EVENT_CLAIM_DEDUP_ENABLED=false` disables).
|
|
52
58
|
|
|
53
59
|
## PII we do not send
|
|
54
60
|
|
|
@@ -57,11 +63,13 @@ Username and hostname *are* sent (per-member usage for the installing org). The
|
|
|
57
63
|
- Prompts, chat transcripts, session titles, and keystrokes
|
|
58
64
|
- Tool arguments, shell commands, and file / code contents
|
|
59
65
|
- Tool error messages, stderr, and result bodies (a failure **class** is sent, never the message)
|
|
60
|
-
- Contents of skills, rules, commands, memory, or settings files (
|
|
66
|
+
- Contents of skills, rules, commands, memory, or settings files **outside** the user-spec path (user-scope name+size listings still omit bodies). User-spec file-backed components **do** send capped `text` (once per content hash; may be deferred under the byte budget); `description` is derived server-side
|
|
67
|
+
- Hook command / prompt strings from the home directory (only `action_kind` + `action_hash`)
|
|
61
68
|
- API keys, tokens, and other secret env vars
|
|
62
69
|
- HTTP(S) proxy credentials and proxy env vars
|
|
63
70
|
- Embedded credentials in git remotes and gateway URLs (`user:pass@` stripped; credential-shaped query params redacted)
|
|
64
|
-
- MCP server commands, args, env blocks, and URLs (name/
|
|
65
|
-
-
|
|
66
|
-
-
|
|
71
|
+
- MCP server commands, args, env blocks, and URLs (name/transport only)
|
|
72
|
+
- Plugin `installPath` / `projectPath` (absolute / home paths)
|
|
73
|
+
- OS username as an identity substitute (git `user.name` / `user.email`, CI actor logins via `platform_login`, or CI emails are sent when available; OS username is still only present via the existing host username field)
|
|
74
|
+
- Home-directory absolute paths (rewritten to home-relative before send), local IP addresses, and listening ports. Exception: the absolute working directory is always sent as `cwd` when stdin has it; `cwd_relative` is added only inside a git repo
|
|
67
75
|
- Data to any endpoint other than the org-configured collector
|
package/README.md
CHANGED
|
@@ -1,30 +1,19 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @ev-ai/agent-hook
|
|
2
2
|
|
|
3
|
-
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
|
|
3
|
+
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`.
|
|
4
4
|
|
|
5
5
|
## What this is (and isn't)
|
|
6
6
|
|
|
7
|
-
This is an **opt-in, first-party observability agent** that an
|
|
7
|
+
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**.
|
|
8
8
|
|
|
9
|
-
- **Data goes to the customer's own collector.** The destination is the org's
|
|
10
|
-
- **It is consent-based and removable.** Nothing installs silently: the operator enables the
|
|
9
|
+
- **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.
|
|
10
|
+
- **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.
|
|
11
11
|
- **Collection is deliberately minimized.** `hook_input` defaults 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_vars` is a fixed allowlist of non-secret CI/runtime markers; API keys and proxy vars are never read. See [Privacy defaults](#privacy-defaults).
|
|
12
12
|
- **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.
|
|
13
13
|
|
|
14
|
-
## Registry access
|
|
15
|
-
|
|
16
|
-
The package is distributed on **GitHub Packages**, so every machine that installs it needs these two lines in `~/.npmrc` with a token that can read packages from the `evalon-ai` org:
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
@evalon-ai:registry=https://npm.pkg.github.com
|
|
20
|
-
//npm.pkg.github.com/:_authToken=<TOKEN>
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Put the token only on the `_authToken` line — never inside the registry URL.
|
|
24
|
-
|
|
25
14
|
## Install
|
|
26
15
|
|
|
27
|
-
**Primary path:** enable the
|
|
16
|
+
**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`.
|
|
28
17
|
|
|
29
18
|
**Fallback:** the `install` CLI merges hooks into those three config files directly.
|
|
30
19
|
|
|
@@ -33,21 +22,21 @@ Put the token only on the `_authToken` line — never inside the registry URL.
|
|
|
33
22
|
1. Write the org destination (manifest only):
|
|
34
23
|
|
|
35
24
|
```bash
|
|
36
|
-
npx @
|
|
37
|
-
--url https://hooks.
|
|
25
|
+
npx @ev-ai/agent-hook@X.Y.Z configure \
|
|
26
|
+
--url https://hooks.ev-ai.ai/<your-org-path-id>/hook
|
|
38
27
|
```
|
|
39
28
|
|
|
40
29
|
`--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.
|
|
41
30
|
|
|
42
|
-
2. Enable the Claude / Cursor / Codex plugins
|
|
43
|
-
3. Commit the shared manifest (`.claude/
|
|
31
|
+
2. Enable the Claude / Cursor / Codex plugins from the hosted marketplace.
|
|
32
|
+
3. 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.
|
|
44
33
|
|
|
45
34
|
Plugins pin an exact package version, never `@latest`.
|
|
46
35
|
|
|
47
|
-
**Migrating from file-wiring:** strip
|
|
36
|
+
**Migrating from file-wiring:** strip ev-ai per-event blocks while keeping the manifest:
|
|
48
37
|
|
|
49
38
|
```bash
|
|
50
|
-
npx @
|
|
39
|
+
npx @ev-ai/agent-hook@X.Y.Z migrate-to-plugin
|
|
51
40
|
```
|
|
52
41
|
|
|
53
42
|
Then enable plugins. Do not leave both plugin and file hooks active — that double-POSTs.
|
|
@@ -61,93 +50,97 @@ Then enable plugins. Do not leave both plugin and file hooks active — that dou
|
|
|
61
50
|
One-time wiring by the repo owner. Teammates pull committed hook configs and need **Node only**.
|
|
62
51
|
|
|
63
52
|
```bash
|
|
64
|
-
npx @
|
|
65
|
-
--url https://hooks.
|
|
53
|
+
npx @ev-ai/agent-hook@latest install \
|
|
54
|
+
--url https://hooks.ev-ai.ai/<your-org-path-id>/hook
|
|
66
55
|
```
|
|
67
56
|
|
|
68
57
|
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`:
|
|
69
58
|
|
|
70
59
|
```bash
|
|
71
|
-
npx @
|
|
72
|
-
--url https://hooks.
|
|
60
|
+
npx @ev-ai/agent-hook@latest install \
|
|
61
|
+
--url https://hooks.ev-ai.ai/<token>/hook \
|
|
73
62
|
--force
|
|
74
63
|
```
|
|
75
64
|
|
|
76
65
|
Skip a vendor entirely with `--no-claude`, `--no-cursor`, or `--no-codex`:
|
|
77
66
|
|
|
78
67
|
```bash
|
|
79
|
-
npx @
|
|
80
|
-
--url https://hooks.
|
|
68
|
+
npx @ev-ai/agent-hook@latest install \
|
|
69
|
+
--url https://hooks.ev-ai.ai/<token>/hook \
|
|
81
70
|
--no-cursor --no-codex
|
|
82
71
|
```
|
|
83
72
|
|
|
73
|
+
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.
|
|
74
|
+
|
|
75
|
+
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).
|
|
76
|
+
|
|
84
77
|
Wired command shape (auth is in the committed `collector_url`). The per-event command is **pinned to the installer's version** (not `@latest`):
|
|
85
78
|
|
|
86
79
|
```bash
|
|
87
|
-
npx -y @
|
|
80
|
+
npx -y @ev-ai/agent-hook@1.0.6 --runner cursor
|
|
88
81
|
```
|
|
89
82
|
|
|
90
|
-
You can optionally put the collector URL on the hook command itself — `--url` (or `--token` + optional `--base-url`) overrides any shared manifest, so `.claude/
|
|
83
|
+
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:
|
|
91
84
|
|
|
92
85
|
```json
|
|
93
86
|
{
|
|
94
|
-
"command": "npx -y @
|
|
87
|
+
"command": "npx -y @ev-ai/agent-hook@1.0.6 --runner claude --url 'https://hooks.ev-ai.ai/<orgToken>/hook'"
|
|
95
88
|
}
|
|
96
89
|
```
|
|
97
90
|
|
|
98
|
-
**Commit** `.claude/
|
|
91
|
+
**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.
|
|
99
92
|
|
|
100
|
-
Install is **additive**: existing non-
|
|
93
|
+
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).
|
|
101
94
|
|
|
102
95
|
### Alternative — CDN binary (no Node at hook time)
|
|
103
96
|
|
|
104
97
|
For environments where Node is not on PATH when runners fire hooks (typically with file-wiring or a future plugin that bundles a binary):
|
|
105
98
|
|
|
106
99
|
```bash
|
|
107
|
-
curl -fsSL https://cdn.
|
|
108
|
-
--url https://hooks.
|
|
100
|
+
curl -fsSL https://cdn.ev-ai.ai/agent-hook/install.sh | sh -s -- -- \
|
|
101
|
+
--url https://hooks.ev-ai.ai/<token>/hook
|
|
109
102
|
```
|
|
110
103
|
|
|
111
104
|
Pin a specific release:
|
|
112
105
|
|
|
113
106
|
```bash
|
|
114
|
-
AGENT_HOOK_VERSION=1.0.6 curl -fsSL https://cdn.
|
|
115
|
-
--url https://hooks.
|
|
107
|
+
AGENT_HOOK_VERSION=1.0.6 curl -fsSL https://cdn.ev-ai.ai/agent-hook/install.sh | sh -s -- -- \
|
|
108
|
+
--url https://hooks.ev-ai.ai/<token>/hook
|
|
116
109
|
```
|
|
117
110
|
|
|
118
|
-
The installer downloads a **standalone binary** (~50–100MB) for your OS/arch, verifies SHA256, caches it under `~/.
|
|
111
|
+
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).
|
|
119
112
|
|
|
120
113
|
Wired command shape (git-root-relative path + `--runner`):
|
|
121
114
|
|
|
122
115
|
```bash
|
|
123
|
-
"$(git rev-parse --show-toplevel)/.
|
|
116
|
+
"$(git rev-parse --show-toplevel)/.ev-ai/agent-hook/ev-ai-agent-hook" --runner cursor
|
|
124
117
|
```
|
|
125
118
|
|
|
126
|
-
**Install per machine** for the binary path: each collaborator runs install once to download their platform binary into `.
|
|
119
|
+
**Install per machine** for the binary path: each collaborator runs install once to download their platform binary into `.ev-ai/agent-hook/` (gitignored).
|
|
127
120
|
|
|
128
121
|
Force npx delivery from Node even when a global binary pin exists:
|
|
129
122
|
|
|
130
123
|
```bash
|
|
131
|
-
npx @
|
|
132
|
-
--url https://hooks.
|
|
124
|
+
npx @ev-ai/agent-hook@latest install \
|
|
125
|
+
--url https://hooks.ev-ai.ai/<token>/hook \
|
|
133
126
|
--delivery npx
|
|
134
127
|
```
|
|
135
128
|
|
|
136
129
|
### Identity and runtime notes
|
|
137
130
|
|
|
138
|
-
Each developer's `installation_id` is created on first hook fire and stored under `~/.
|
|
131
|
+
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.
|
|
139
132
|
|
|
140
|
-
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 `
|
|
133
|
+
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.
|
|
141
134
|
|
|
142
135
|
### Gitignore (recommended)
|
|
143
136
|
|
|
144
137
|
```gitignore
|
|
145
|
-
/.
|
|
138
|
+
/.ev-ai/
|
|
146
139
|
```
|
|
147
140
|
|
|
148
|
-
Do **not** gitignore `.claude/
|
|
141
|
+
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.
|
|
149
142
|
|
|
150
|
-
For the **plugin** path, do not add
|
|
143
|
+
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.
|
|
151
144
|
|
|
152
145
|
## Fail-open contract
|
|
153
146
|
|
|
@@ -156,42 +149,42 @@ For the **plugin** path, do not add Evalon per-event hooks to `.claude/settings.
|
|
|
156
149
|
- 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
|
|
157
150
|
- POST / spawn failures log to stderr only
|
|
158
151
|
|
|
159
|
-
Debug envelope logging: `
|
|
152
|
+
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).
|
|
160
153
|
|
|
161
154
|
## Privacy defaults
|
|
162
155
|
|
|
163
156
|
Full field-by-field inventory of what is POSTed: [COLLECTED-DATA.md](./COLLECTED-DATA.md).
|
|
164
157
|
|
|
165
|
-
- **`hook_input`**: allowlisted metadata only (`session_id`, `tool_name`, `status`, …). Tool args / prompts / session titles are dropped. Opt-in full stdin: untracked operator manifest `capture_hook_input: "full"` or `
|
|
158
|
+
- **`hook_input`**: allowlisted metadata only (`session_id`, `tool_name`, `status`, …). Tool args / prompts / session titles are dropped. Opt-in full stdin: untracked operator manifest `capture_hook_input: "full"` or `EV_AI_HOOK_CAPTURE_FULL_INPUT=1` (still size-capped). A **git-tracked** (committed) manifest cannot set `full` — use the env var or an untracked operator manifest instead.
|
|
166
159
|
- **`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.
|
|
167
160
|
- **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).
|
|
168
161
|
|
|
169
162
|
## Uninstall
|
|
170
163
|
|
|
171
|
-
**Plugin path:** disable / uninstall the
|
|
164
|
+
**Plugin path:** disable / uninstall the ev-ai marketplace plugins, then remove or rotate the shared manifest.
|
|
172
165
|
|
|
173
|
-
**File-wiring → plugin:** keep the manifest, strip only
|
|
166
|
+
**File-wiring → plugin:** keep the manifest, strip only ev-ai file hooks:
|
|
174
167
|
|
|
175
168
|
```bash
|
|
176
|
-
npx @
|
|
169
|
+
npx @ev-ai/agent-hook migrate-to-plugin
|
|
177
170
|
```
|
|
178
171
|
|
|
179
172
|
**File-wiring full uninstall** (also deletes the shared manifest):
|
|
180
173
|
|
|
181
174
|
```bash
|
|
182
|
-
npx @
|
|
175
|
+
npx @ev-ai/agent-hook uninstall
|
|
183
176
|
```
|
|
184
177
|
|
|
185
178
|
Or via the CDN-installed binary:
|
|
186
179
|
|
|
187
180
|
```bash
|
|
188
|
-
"$HOME/.
|
|
181
|
+
"$HOME/.ev-ai/agent-hook/current/ev-ai-agent-hook" uninstall
|
|
189
182
|
```
|
|
190
183
|
|
|
191
184
|
Or:
|
|
192
185
|
|
|
193
186
|
```bash
|
|
194
|
-
curl -fsSL https://cdn.
|
|
187
|
+
curl -fsSL https://cdn.ev-ai.ai/agent-hook/install.sh | sh -s -- uninstall
|
|
195
188
|
```
|
|
196
189
|
|
|
197
|
-
`uninstall` removes
|
|
190
|
+
`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) or `~/.ev-ai/agent-hook/` (per-machine identity).
|