@ev-ai/agent-hook 1.1.6 → 1.2.0
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 +17 -13
- package/README.md +21 -6
- package/dist/cli.mjs +40 -32
- package/dist/hook.mjs +35 -27
- package/dist/launcher-update.mjs +5 -0
- package/dist/launcher.mjs +5 -0
- package/dist/managed-hooks-snippet.cjs +177 -0
- package/dist/managed-hooks-snippet.d.ts +3 -0
- package/dist/managed-hooks-snippet.mjs +149 -0
- package/dist/plugin-hooks.mjs +39 -26
- package/package.json +15 -2
package/COLLECTED-DATA.md
CHANGED
|
@@ -10,7 +10,7 @@ What `@ev-ai/agent-hook` POSTs to the ev-ai collector (`POST /{orgToken}/hook`,
|
|
|
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` (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`, `build_ms` (wall-clock milliseconds to assemble the envelope before the collector POST; not tool/session duration), and `self_config.operator` (human identity — see Operator identity below; skipped when `EV_AI_HOOK_USER_SPEC=off`).
|
|
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`, `build_ms` (wall-clock milliseconds to assemble the envelope before the collector POST; not tool/session duration), and `self_config.operator` (human identity — see Operator identity below; skipped when `EV_AI_HOOK_USER_SPEC=off`). When the process was started by the npx launcher (`dist/launcher.mjs`), the envelope also includes `launcher_version`, `hook_source` (`bundled` / `cached` / `bootstrap`), `hook_fallback_reason` (string or `null`), and `launcher_scope_match` (whether the hook's POST URL hashes to the same collector scope the launcher resolved). Direct `node dist/hook.mjs`, the compiled binary, and the marketplace plugin launcher omit those four fields. On `session_start` only, a launcher-started hook also adds `launcher_scope` (the opaque collector-scope hash from `EV_AI_LAUNCHER_SCOPE`, never a token) and `launcher_attempt` (`n`, `latest_claim`, `outcome`, `finished_at`, `selection_version`). `outcome` is a `.done` result (`updated`, `current`, `repaired`, `bundled`, `skipped:below_floor`, `skipped:quarantined`, `failed:*`, …) or `incomplete` when the latest claim has no `.done` past its owner budget. A log read error omits `launcher_attempt` and never delays the runner ack. Other events carry only the four launcher fields above.
|
|
14
14
|
|
|
15
15
|
## Tracked data points
|
|
16
16
|
|
|
@@ -30,18 +30,19 @@ Every event also carries: UTC timestamp, `installation_id` (per-machine under `~
|
|
|
30
30
|
|
|
31
31
|
### On `session_end`
|
|
32
32
|
|
|
33
|
-
- Stop `status` / `stop_reason` / SessionEnd `reason`, and Claude `StopFailure` `error` only when it is identifier-shaped (`rate_limit`); error prose is dropped
|
|
33
|
+
- Stop `status` / `stop_reason` / SessionEnd `reason`, and Claude `StopFailure` `error` only when it is identifier-shaped (`rate_limit`); error prose is dropped. Successful per-turn `Stop` / `stop` with no stdin status is recorded as `completed`
|
|
34
|
+
- **`last_assistant_message`** — Claude Stop last-assistant text when content capture is on (empty string when the key is absent, so the path exists). Dropped on `StopFailure`, where the field carries runner error prose rather than a model reply — the same prose the `error` / `error_details` drop exists to keep out. Full stdin capture still forwards it
|
|
34
35
|
- **`model`** (Claude only, when readable) — last assistant `message.model` from `transcript_path` on Stop/SessionEnd; reflects mid-session `/model` or consent fallback, not just SessionStart settings snapshot
|
|
35
36
|
- Allowlisted hook metadata (see below)
|
|
36
37
|
|
|
37
38
|
### On `tool_invoked`
|
|
38
39
|
|
|
39
40
|
- `tool_name`, `skill_name`, `rule_name`, `command_name`
|
|
40
|
-
- **`executable`** — first token of a shell or MCP spawn command when stdin has one (`tool_input.command` or Cursor top-level `command`).
|
|
41
|
-
- **Cursor `skill_name` (derived, not from a native Skill tool):** slash commands via `beforeSubmitPrompt` (`/my-skill` → `tool_name: Skill`, `skill_name: my-skill`; prompt
|
|
42
|
-
- **Claude `skill_name` (derived):** slash commands via `UserPromptExpansion` (`command_name` → `tool_name: Skill`, `skill_name: <id>`; `prompt`
|
|
43
|
-
- **Codex `skill_name` (derived):** slash / custom-prompt ids via `UserPromptSubmit` (`/my-skill` → `tool_name: Skill`, `skill_name: my-skill`; prompt
|
|
44
|
-
- **Cursor `rule_name` (derived):** always-apply rules via `beforeSubmitPrompt` `attachments[]` (`type: "rule"` → `tool_name: Rule`; `file_path` / `attachments`
|
|
41
|
+
- **`executable`** — first token of a shell or MCP spawn command when stdin has one (`tool_input.command` or Cursor top-level `command`). Payload `executable` never includes args or the rest of the line. The full Cursor `command` string and Claude `tool_input` object ride on `hook_input` unless content capture is off. Home-directory prefixes on `executable` are rewritten to `~`. Omitted when the tool has no command (Read / Write / Grep / Skill / MCP tool calls).
|
|
42
|
+
- **Cursor `skill_name` (derived, not from a native Skill tool):** slash commands via `beforeSubmitPrompt` (`/my-skill` → `tool_name: Skill`, `skill_name: my-skill`; the prompt body is on `hook_input.prompt` unless content capture is off); 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; the shell command line is on `hook_input.command` unless content capture is off; argv0 may appear as `executable`). Silent auto-inject with no slash and no SKILL.md Read is not observable.
|
|
43
|
+
- **Claude `skill_name` (derived):** slash commands via `UserPromptExpansion` (`command_name` → `tool_name: Skill`, `skill_name: <id>`; `prompt` is on `hook_input` unless content capture is off; `command_args` are never POSTed except under full stdin). Typing `/skillname` bypasses `PreToolUse(Skill)`; Read of `**/skills/<id>/SKILL.md` on Pre/Post tool hooks is also derived when present. Non-slash user prompts arrive on `UserPromptSubmit`.
|
|
44
|
+
- **Codex `skill_name` (derived):** slash / custom-prompt ids via `UserPromptSubmit` (`/my-skill` → `tool_name: Skill`, `skill_name: my-skill`; the prompt body is on `hook_input.prompt` unless content capture is off); Bash/Shell whose `tool_input.command` references `.claude/.cursor/.agents/skills/<id>/SKILL.md` or `.codex/commands/<id>.md` (id only; `tool_input` is on `hook_input` unless content capture is off; argv0 may appear as `executable`); Read-like tools of those paths when present. Silent auto-inject with no slash and no skill-file touch is not observable.
|
|
45
|
+
- **Cursor `rule_name` (derived):** always-apply rules via `beforeSubmitPrompt` `attachments[]` (`type: "rule"` → `tool_name: Rule`; `file_path` / `attachments` are never POSTed; prompt text is on `hook_input.prompt` unless content capture is off). When the attachment resolves to a local rules file, `rule_name` is the path-derived id after symlink canonicalization (extension stripped): Cursor's `.cursor/rules/claude/_global/finops.mdc` shim names as `_global/finops` because that path realpaths to `.claude/rules/_global/finops.mdc`. Glob / agent-requestable / @-mentioned rules are not on submit stdin — only a later Read is observable. Root memory files Cursor attaches as rules (`CLAUDE.md`, `AGENTS.md`) emit `rule_name` without a `component_reference`.
|
|
45
46
|
- **Claude `rule_name` (derived):** Claude Code has no submit-time attachment list, so a rule is only observable as a Read / Shell that touches a rules file. Read of `**/.cursor/rules/**`, `**/.claude/rules/**`, or `**/.codex/rules/**` on any Pre/Post tool hook emits `rule_name` (id only — the file path is never POSTed), and on any runner that path is realpath-canonicalized before naming, so a tool event and an attachment event on the same file agree. A Shell command that references a rules path yields the id from the stdin spelling without canonicalization. Auto-attached rules that are never read are not observable.
|
|
46
47
|
- **`command_name` (derived, all runners):** the slash-command id when a slash invocation resolves to a command file rather than a `SKILL.md` (`.claude/commands/**`, `.cursor/commands/**`, `.codex/prompts/**`), and the path-derived id on a Read / Shell that touches one of those files. Sent alongside `skill_name` on slash events, never instead of it. Namespaced commands keep their directory prefix (`git/commit`). When the same id exists under more than one commands root, the running runner's root is tried first (Claude → `.claude/commands`, Cursor → `.cursor/commands`, Codex → `.codex/prompts`).
|
|
47
48
|
- `tool_use_id` — opaque Claude Pre/Post join key when present in stdin
|
|
@@ -49,16 +50,18 @@ Every event also carries: UTC timestamp, `installation_id` (per-machine under `~
|
|
|
49
50
|
- `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.
|
|
50
51
|
- `duration_ms` — from Claude `duration_ms` or Cursor `duration` on post-tool / `afterShellExecution` stdin
|
|
51
52
|
- `mcp_server` / `mcp_tool` — separate fields, never a `server__tool` blob. Cursor `beforeMCPExecution` / `afterMCPExecution` stdin `mcp_server_name` (the mcp.json key) is the server when present; otherwise derived from opaque `tool_name` when it matches Claude `mcp__<server>__<tool>` or Cursor `MCP:<server>:<tool>` / `MCP:<server>__<tool>` / `MCP:<tool>` (no `tool_input` parsing). `mcp_server_name` itself is not POSTed. Claude has no equivalent stdin field, so the `mcp__` form is split against the **declared** `mcpServers` keys first — a declared name whose tool-name spelling (every character outside `[A-Za-z0-9_-]` replaced, `acme.tools` → `acme_tools`) prefixes the rest wins, longest first, and `mcp_server` is then the config key rather than the runner's spelling. Splitting on the first `__` is only the fallback for a server that is not declared locally (claude.ai connectors, plugin servers), so a tool id that itself contains `__` (`mcp__aws__aws___call_aws` → tool `aws___call_aws`) and a server name that contains `__` both survive.
|
|
52
|
-
- **`mcp_package_name` / `mcp_package_spec` / `mcp_package_version` / `mcp_package_version_source`** — npm identity of an MCP server launched through `npx` / `npm exec`, resolved on MCP live events. Cursor `beforeMCPExecution` carries the spawn command
|
|
53
|
+
- **`mcp_package_name` / `mcp_package_spec` / `mcp_package_version` / `mcp_package_version_source`** — npm identity of an MCP server launched through `npx` / `npm exec`, resolved on MCP live events. Cursor `beforeMCPExecution` carries the spawn command, which is tokenized in-process for package identity and never POSTed — that argv routinely embeds `--api-key <secret>`. Claude Pre/Post MCP tool hooks — and Cursor MCP tool hooks — have no spawn command on stdin, so the same identity is resolved from the declared server's `command`/`args` in MCP config (`~/.claude.json` — top-level plus the launch directory's `projects[<cwd>].mcpServers`, where Claude keeps `--scope local` servers — `.mcp.json`, `.cursor/mcp.json`); that launch line is still never POSTed. What leaves the machine is the package name (`@playwright/mcp`), the spec as declared (`@playwright/mcp@latest`), and the version npm actually installed for it. `mcp_package_version` is read from npm's own `_npx` install tree — the directory npm names from a hash of the package specs — and is therefore what really ran, not what config asked for; `mcp_package_version_source` records that provenance (`npx_cache`) so a declared pin is never mistaken for an observed install. A floating tag reports its resolved version; a cache miss reports the spec with no version, which is normal before the runner has launched the server once, after a failed or cleaned install, and for a package resolved from a local or global `node_modules` (npm creates no `_npx` tree then). HTTP / SSE servers, plain binaries, `uvx`, local paths, tarball and git specs report nothing. A bare flag of unknown arity is a hard stop rather than a guess, so `npx --api-key <secret> server` reports no package at all — mis-reading the argv would publish a credential as a package name. A `Bash` tool that happens to run `npx` is never reported as a server.
|
|
53
54
|
- `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; exact rule paths under a layout rules root map to RULE installations; exact command paths under a layout commands root map to COMMAND installations. Name-only, flat/unattributed MCP, malformed scope, and missing platform-agent identity stay unmatched. Three surfaces produce a file-backed reference: a slash-command event when a matching `SKILL.md` or command file is found locally, a Cursor rule-attachment event when a matching rules file is found locally, and any tool event whose stdin path resolves under a layout skills / rules / commands root — which is the only route Claude has for non-MCP components. On a tool event the MCP reference wins when both could apply, and a Write / edit of one of those files does not count as usage. The resolved path is never POSTed. Absolute local paths never leave the hook process: path evidence is realpath-canonicalized (so a `.cursor/rules/claude` symlink to `.claude/rules` hashes the git path) then stripped to the repository-relative source path before hashing, and only the opaque reference is sent.
|
|
54
55
|
- `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
|
|
55
56
|
- Allowlisted hook metadata (see below)
|
|
56
57
|
|
|
57
58
|
### Hook metadata (default)
|
|
58
59
|
|
|
59
|
-
From runner stdin
|
|
60
|
+
From runner stdin: 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`), `cwd` (stdin when present; else `process.cwd()` when it is a git work tree or under `CURSOR_PROJECT_DIR`; else `CURSOR_PROJECT_DIR` — Cursor user-level hooks run from `~/.cursor`, which is not the workspace; extra Agents Window folders are not on the payload; the env path is never shipped as an env var; home prefix rewritten to `~` when the path is under the user home, otherwise absolute POSIX), and git-root-relative `cwd_relative` in addition when the session is inside a git repo and that resolved cwd is under the git root. `debug: true` on an untracked / home overlay is a third route to full stdin; it does not change the collector URL.
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
**Content fields (sent unless killed):** `prompt`, `tool_input`, `tool_response`, `last_assistant_message`, and Cursor top-level `command`. Two per-event exclusions apply: `command` is dropped on `beforeMCPExecution` (MCP spawn argv carries credentials) and `last_assistant_message` on `StopFailure` (runner error prose). Oversize values are clipped head+tail to a shared per-string budget; if that still cannot reach the 256 KiB cap, the largest fields are dropped (`_dropped_keys`) so the event's metadata always ships rather than the whole POST being rejected. Kill switch: `EV_AI_HOOK_CAPTURE_CONTENT=off` or `capture_hook_input: "allowlist"`. Session titles, `command_args`, transcript paths, file paths, file contents, edits, MCP URLs, and runner error prose stay dropped. Full stdin dump: `capture_hook_input: "full"` or `EV_AI_HOOK_CAPTURE_FULL_INPUT=1` (still size-capped). The first token of a shell or MCP spawn command is also sent separately as payload `executable` (see above).
|
|
63
|
+
|
|
64
|
+
Install wires Claude `SessionStart` / `UserPromptExpansion` / `UserPromptSubmit` / `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).
|
|
62
65
|
|
|
63
66
|
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).
|
|
64
67
|
|
|
@@ -66,9 +69,10 @@ When Cursor loads Claude (or Codex) file hooks as third-party configs alongside
|
|
|
66
69
|
|
|
67
70
|
Username and hostname *are* sent (per-member usage for the installing org). The following are not:
|
|
68
71
|
|
|
69
|
-
-
|
|
70
|
-
- Tool
|
|
71
|
-
-
|
|
72
|
+
- Chat transcripts (the JSONL file), session titles, and keystrokes
|
|
73
|
+
- Tool error messages and stderr (a failure **class** is sent, never the message). Tool **result bodies** (`tool_response`) and **arguments** (`tool_input` / Cursor `command`) **are** sent unless `EV_AI_HOOK_CAPTURE_CONTENT=off`
|
|
74
|
+
- User **prompts** and the last assistant message **are** sent unless `EV_AI_HOOK_CAPTURE_CONTENT=off`
|
|
75
|
+
- Cursor top-level `content` / `edits` / `file_path` from `afterFileEdit` / `beforeReadFile` stdin — those stay dropped. **Claude nests the same data inside `tool_input`** (Write `content`, Edit `old_string` / `new_string`), and `tool_response` carries read / command output, so on Claude those bodies **are** sent unless `EV_AI_HOOK_CAPTURE_CONTENT=off`
|
|
72
76
|
- 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
|
|
73
77
|
- Hook **prompt** strings (only `action_kind` + `action_hash` for `prompt`-type actions). Hook **command** strings (`command` / `bash` / `powershell`) *are* sent, sanitized: home directory rewritten to `~`, URLs/tokens/secret-shaped args redacted, capped at 300 chars
|
|
74
78
|
- API keys, tokens, and other secret env vars
|
package/README.md
CHANGED
|
@@ -8,12 +8,12 @@ This is an **opt-in, first-party observability agent** that an ev-ai customer in
|
|
|
8
8
|
|
|
9
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
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
|
-
- **Collection is
|
|
12
|
-
- **What it is not:** it is not spyware, not a keylogger, and not a data broker. It does not collect keystrokes,
|
|
11
|
+
- **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](#privacy-defaults).
|
|
12
|
+
- **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.
|
|
13
13
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
16
|
-
**Primary path:** enable the ev-ai **runner plugins** (Claude / Cursor / Codex) from the hosted marketplace. Their hooks
|
|
16
|
+
**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`.
|
|
17
17
|
|
|
18
18
|
**Fallback:** the `install` CLI merges hooks into those three config files directly.
|
|
19
19
|
|
|
@@ -31,7 +31,7 @@ npx @ev-ai/agent-hook@X.Y.Z configure \
|
|
|
31
31
|
2. Enable the Claude / Cursor / Codex plugins from the hosted marketplace.
|
|
32
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.
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
File-wiring pins an exact package version, never `@latest`. Plugin runtime bumps ride the shipped `hook.mjs` payload.
|
|
35
35
|
|
|
36
36
|
**Migrating from file-wiring:** strip ev-ai per-event blocks while keeping the manifest:
|
|
37
37
|
|
|
@@ -132,6 +132,21 @@ Each developer's `installation_id` is created on first hook fire and stored once
|
|
|
132
132
|
|
|
133
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.
|
|
134
134
|
|
|
135
|
+
### Hook updates (npx path)
|
|
136
|
+
|
|
137
|
+
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/`, and otherwise the hook bundled in the pinned package. A cached hook is re-verified (sha256) on every event, and any problem with it (missing, altered, incompatible with this Node or launcher, or failing to start twice in a row) falls back to the bundled hook for that event.
|
|
138
|
+
|
|
139
|
+
**Network.** Besides the collector POST, the launcher makes two kinds of request, both only on the npx path:
|
|
140
|
+
|
|
141
|
+
- `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.
|
|
142
|
+
- When the collector names a release, a download of that release's npm tarball from `https://registry.npmjs.org/@ev-ai/agent-hook/-/`. The tarball is checked against the sha512 integrity the collector sent before it is unpacked, and only `package.json` and `dist/hook.mjs` are extracted. No other destination is contacted.
|
|
143
|
+
|
|
144
|
+
**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.
|
|
145
|
+
|
|
146
|
+
**Status.** `npx @ev-ai/agent-hook@X.Y.Z 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.
|
|
147
|
+
|
|
148
|
+
**Cache.** Deleting `~/.ev-ai/agent-hook/js/` is safe at any time: the next event runs the bundled hook and checks again.
|
|
149
|
+
|
|
135
150
|
### Gitignore (recommended)
|
|
136
151
|
|
|
137
152
|
```gitignore
|
|
@@ -155,7 +170,7 @@ Debug envelope logging: `EV_AI_HOOK_DEBUG=1` (redacted summary). Full envelope:
|
|
|
155
170
|
|
|
156
171
|
Full field-by-field inventory of what is POSTed: [COLLECTED-DATA.md](./COLLECTED-DATA.md).
|
|
157
172
|
|
|
158
|
-
- **`hook_input`**:
|
|
173
|
+
- **`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.
|
|
159
174
|
- **`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.
|
|
160
175
|
- **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).
|
|
161
176
|
- **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`.
|
|
@@ -188,4 +203,4 @@ Or:
|
|
|
188
203
|
curl -fsSL https://cdn.ev-ai.ai/agent-hook/install.sh | sh -s -- uninstall
|
|
189
204
|
```
|
|
190
205
|
|
|
191
|
-
`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
|
|
206
|
+
`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/`).
|