@ev-ai/agent-hook 1.3.4 → 1.3.6
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 +3 -3
- package/README.md +3 -3
- package/dist/cli.mjs +40 -40
- package/dist/hook.mjs +35 -35
- package/dist/launcher-update.mjs +4 -4
- package/dist/launcher.mjs +6 -3
- package/dist/managed-hooks-snippet.cjs +3 -15
- package/dist/managed-hooks-snippet.mjs +3 -15
- package/dist/plugin-hooks.mjs +35 -47
- package/package.json +2 -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`). 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.
|
|
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`, `hook_delivery` (`plugin` when a runner plugin started the hook, `npx` when an npx hook command did, otherwise `null`), `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
|
|
|
@@ -40,7 +40,7 @@ Every event also carries: UTC timestamp, `installation_id` (per-machine under `~
|
|
|
40
40
|
- `tool_name`, `skill_name`, `rule_name`, `command_name`
|
|
41
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
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
|
|
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` only when prompt capture is on; with capture off that event is not sent. Slash-command rows from the same hook still send, without the prompt.
|
|
44
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
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`.
|
|
46
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.
|
|
@@ -79,7 +79,7 @@ Username and hostname *are* sent (per-member usage for the installing org). The
|
|
|
79
79
|
|
|
80
80
|
- Chat transcripts (the JSONL file), session titles, and keystrokes
|
|
81
81
|
- 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`
|
|
82
|
-
- User **prompts**, tool args, tool results and the last assistant message — dropped unless `EV_AI_HOOK_CAPTURE_PROMPT=on`, the org config `collect_prompt`, or full stdin capture
|
|
82
|
+
- User **prompts**, tool args, tool results and the last assistant message — dropped unless `EV_AI_HOOK_CAPTURE_PROMPT=on`, the org config `collect_prompt`, or full stdin capture. With prompt capture off, a plain prompt submit sends no event at all; slash-command and rule rows from that hook still send, without the prompt.
|
|
83
83
|
- 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`
|
|
84
84
|
- 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
|
|
85
85
|
- 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
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ 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
|
-
- **Prompt collection is opt-in, and it covers every content body.** User prompts, tool args, tool results and the last assistant message (`hook_input.prompt`, `tool_input`, `tool_response`, `last_assistant_message`, Cursor `command`) are dropped by default and sent together (size-capped) with `EV_AI_HOOK_CAPTURE_PROMPT=on` or the org config `collect_prompt`. Policy rules still match them locally either way.
|
|
11
|
+
- **Prompt collection is opt-in, and it covers every content body.** User prompts, tool args, tool results and the last assistant message (`hook_input.prompt`, `tool_input`, `tool_response`, `last_assistant_message`, Cursor `command`) are dropped by default and sent together (size-capped) with `EV_AI_HOOK_CAPTURE_PROMPT=on` or the org config `collect_prompt`. With prompt capture off, a plain prompt submit sends no event at all (Claude/Codex `UserPromptSubmit`, Cursor `beforeSubmitPrompt` with no slash command or rule attachment). Slash-command and rule rows from those hooks still send, without the prompt. Policy rules still match them locally either way.
|
|
12
12
|
- **Content kill switch.** `EV_AI_HOOK_CAPTURE_CONTENT=off` or `capture_hook_input: "allowlist"` drops all of them even when prompt collection is on. 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).
|
|
13
13
|
- **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 forwards the bodies of prompts and tool calls only when prompt collection is turned on.
|
|
14
14
|
|
|
@@ -106,7 +106,7 @@ Keep Claude and Cursor both wired. Cursor also loads `.claude/settings.json` as
|
|
|
106
106
|
|
|
107
107
|
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).
|
|
108
108
|
|
|
109
|
-
**Concurrency cap.** At most `max_concurrent_hooks`
|
|
109
|
+
**Concurrency cap.** At most `max_concurrent_hooks` events (set in `~/.ev-ai/config.json`; default 4 on Windows, 10 elsewhere) are handled at the same time. Each one holds a slot file under `~/.ev-ai/agent-hook/slots/`. The launcher reserves a slot before it starts the hook and passes it along, so the hook and its background worker share that one slot; the two overlap briefly while the hook exits, so up to twice that many `hook.mjs` processes can be alive for a moment. An event that arrives while every slot is held is dropped: the runner still gets its normal ack and no hook process starts. Session start and end events are never capped. A value that is not a positive integer falls back to the default. A slot-file error fails open (the event is collected) unless other slots are held, in which case the event is dropped.
|
|
110
110
|
|
|
111
111
|
Wired command shape (auth is in the committed `collector_url`). The per-event command is **pinned to the installer's version** (not `@latest`):
|
|
112
112
|
|
|
@@ -204,7 +204,7 @@ Debug envelope logging: `EV_AI_HOOK_DEBUG=1` (redacted summary). Full envelope:
|
|
|
204
204
|
|
|
205
205
|
Full field-by-field inventory of what is POSTed: [COLLECTED-DATA.md](./COLLECTED-DATA.md).
|
|
206
206
|
|
|
207
|
-
- **`hook_input`**: metadata (session/tool ids, status, …). The content fields — `prompt`, `tool_input`, `tool_response`, `last_assistant_message`, and Cursor `command` — are opt-in together: `EV_AI_HOOK_CAPTURE_PROMPT=on` (also accepts `1` / `true`) or the org config `collect_prompt`. 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.
|
|
207
|
+
- **`hook_input`**: metadata (session/tool ids, status, …). The content fields — `prompt`, `tool_input`, `tool_response`, `last_assistant_message`, and Cursor `command` — are opt-in together: `EV_AI_HOOK_CAPTURE_PROMPT=on` (also accepts `1` / `true`) or the org config `collect_prompt`. With prompt capture off, a plain prompt submit sends no event; slash-command and rule rows from that hook still send, without the prompt. 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.
|
|
208
208
|
- **`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.
|
|
209
209
|
- **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).
|
|
210
210
|
- **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`.
|