@ev-ai/agent-hook 1.2.8 → 1.3.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 CHANGED
@@ -21,9 +21,9 @@ Every event also carries: UTC timestamp, `installation_id` (per-machine under `~
21
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. Sources: the manifest, `.mcp.json` / `.cursor/mcp.json` in the repo, and the runner's user file (`~/.claude.json` top-level `mcpServers` plus `projects[<cwd>].mcpServers` for the launch directory only — never another project's servers; `~/.cursor/mcp.json`). The resolved npm package + version for `npx`-launched servers rides on the user-spec MCP row and on `tool_invoked` MCP live events instead (see below).
23
23
  - **User-scope config** — `cli_version` (claude / agent|cursor-agent / codex `--version`); model (Claude; SessionStart stdin `model` preferred over settings-file, then admin-managed settings, then the untracked project override, then user settings — the committed `.claude/settings.json` is deliberately skipped because the git pipeline already sees it), Claude-only `claude_version` mirror; presence/size of global settings / memory / MCP files and name+size listings of global skills, commands, subagents, rules — union of every runner's home surfaces (Claude, Cursor, Codex), not filtered by which runner started the session. The settings probe also covers the admin-managed layer (Claude `~/.claude/remote-settings.json` plus `managed-settings.json` and `managed-settings.d/*.json`, Cursor enterprise `hooks.json` and cloud-synced team hooks under `~/.cursor/managed/<team>/`, Codex `requirements.toml` plus `managed_config.toml` in both its system and `~/.codex/` locations) (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 on this presence/size probe — allowlisted sandbox and permissions posture from those files ships on the user-spec `SETTINGS` / `MANAGED_SETTINGS` rows below.
24
- - **User spec (home-dir components)** — per-component delta of skills / commands / rules / memory / subagents / hooks / settings / MCP servers / plugins from `~/.claude`, `~/.cursor`, `~/.codex`, `~/.agents`, and from the home root itself (`~/CLAUDE.md`, `~/AGENTS.md` — Claude's and Codex's memory walk reaches them). Hook rows are parsed out of every settings file each runner reads, not just the dedicated hooks file: Claude `settings.json` / `settings.local.json` / `hooks.json`, Cursor `hooks.json` / `sandbox.json` / `permissions.json` / `cli-config.json`, Codex `hooks.json` — plus the admin-managed layer (Claude `managed-settings.json` with its `managed-settings.d/*.json` drop-ins **and** `~/.claude/remote-settings.json`, the cached payload from the claude.ai console; Cursor enterprise `hooks.json` and cloud-synced team hooks under `~/.cursor/managed/<team>/`). Admin-managed sources are org-written rather than operator-written, so they are reported as that tier even when the file itself sits under home. Allowlisted sandbox / permissions / MCP-toggle keys from those JSON files also ship as `SETTINGS` (operator-written) or `MANAGED_SETTINGS` (admin-written) rows: one row per existing file, `name: "settings"`, nested `attributes.settings` (same shape as the parsed object, not a JSON string), plus `keys_omitted` (top-level unread key **names** only — values never leave the machine). `hooks{}` is excluded from `settings` because it already ships as HOOK rows (the name still appears in `keys_omitted`). Never shipped as values: Claude `env`, `apiKeyHelper`, `awsAuthRefresh`, `awsCredentialExport`, `otelHeadersHelper`; Cursor `authInfo`. A missing file produces no row; a file that exists but is not a valid JSON object (including JSONC) produces a row with `parse_error: true`, and one that exists but cannot be read (e.g. permission denied) produces `read_error: true` — neither carries `settings`. A leading UTF-8 byte-order mark (written by Windows editors and PowerShell 5) is ignored. Claude allowlist: `permissions` (whole object), `sandbox` (whole object), `enableAllProjectMcpServers`, `enabledMcpjsonServers`, `disabledMcpjsonServers`, `disableAllHooks`, `disableAutoMode`, and the managed-only locks `allowManagedHooksOnly`, `allowManagedPermissionRulesOnly`, `allowManagedMcpServersOnly`, `strictKnownMarketplaces`, `strictPluginOnlyCustomization`, `disableSideloadFlags`, `forceLoginMethod`, `forceLoginOrgUUID`. Cursor `sandbox.json`: `type`, `additionalReadwritePaths`, `additionalReadonlyPaths`, `disableTmpWrite`, `enableSharedBuildCache`, `networkPolicy`. Cursor `permissions.json`: `terminalAllowlist`, `mcpAllowlist`, `autoRun`. Cursor CLI (`cli-config.json` at home, `cli.json` at a project): `permissions` only. Permission rule strings (Claude `permissions.allow|deny|ask`, Cursor CLI tokens, `terminalAllowlist`, `sandbox.excludedCommands`, `mcpAllowlist`) have absolute paths rewritten to `<path>`; filesystem path lists (`sandbox.filesystem.*`, `sandbox.credentials.files[].path`, `permissions.additionalDirectories`, Cursor `additionalReadwritePaths` / `additionalReadonlyPaths`) rewrite the home prefix to `~` without collapsing the rest of the path (separators normalized to `/`; a Windows home such as `C:\Users\me` matches case-insensitively), and so do Unix socket paths in `sandbox.network.allowUnixSockets`. Every other string, rule strings included, also has the home directory replaced with `~` wherever it appears, with either separator, so no allowlisted value carries the OS username. Arrays are capped at 200 entries, strings at 200 characters, nesting at 6 levels, and serialized `settings` at 16 KiB: over the byte cap the longest lists lose trailing entries first, and a top-level key is dropped only when no list is left to shorten. When any cap hits the row carries `truncated: true` and `trimmed_paths` (the dotted paths shortened or removed). If the collector still cannot store `settings` it keeps the row and sets `settings_dropped: true`. Claude managed files add `managed_source` (`server` / `file` / `file_dropin`). Codex TOML settings (`config.toml`, `managed_config.toml`, `requirements.toml`) are presence/size only apart from their `[mcp_servers]` sections. Not collected: `$CLAUDE_CONFIG_DIR` (no walker honours it today); Claude managed policy delivered only by MDM plist / Windows registry (not a readable file path); Cursor team-admin sandbox/permissions policy (dashboard-side, not on disk). Project-directory spec is a sibling snapshot (next bullet), not omitted from this envelope. 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*`. A session start on which the launcher also runs its background update check is collected by that check once it finishes (at most 10 s later), on whatever version it selected, so its user-spec walk runs as usual. Each upsert row carries `kind`, path-derived `name`, home-relative `path`, capped file `text` (≤100 KiB) 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 2 MiB user-spec byte budget, overflowing bodies are deferred (`user_spec_text_deferred`) before rows are dropped. Self-config caches commit only after a successful collector POST. Operator identity is attached on the POST itself (every event), not on this cooldown. Hook rows send `action_kind` + `action_hash` plus a sanitized `command` (home directory rewritten to `~`, URLs/tokens/secret-shaped args redacted, capped at 300 chars) — never the raw unsanitized string. Prompt-type hook actions send only `action_kind` + `action_hash`. MCP rows send name, `transport`, a credential-stripped `command_or_url` (from `command` / `url` / `serverUrl`), redacted `args`, env var **names** (from `env` / `envVars` / `env_vars` / `bearer_token_env_var` / `env_http_headers` values), `headers_present`, `env_http_header_names`, `auth`, `bearer_token_env_var`, `default_tools_approval_mode`, declared tool allow/deny lists (`tools`, `auto_approve`, `always_allow`, `enabled_tools`, `disabled_tools` — tool names verbatim, boolean `auto_approve` when not a list, `<attr>_truncated` when a list is capped), and — for a server launched via `npx` / `npm exec` — `package_name` / `package_spec` / `package_version` / `package_version_source` resolved from npm's `_npx` install tree exactly as on `tool_invoked` above (so a floating `@latest` row's `content_hash` moves when the installed version changes, which is the point: the row re-sends as new evidence) — never env or header values; a server switched off in config (`enabled: false` / `disabled: true`) is reported with the row's `enabled` flag false rather than as live surface. Every other declared config key is surfaced by name with its value replaced by `<omitted>`, so the config shape is visible without its contents. Plugin rows never include `installPath` / `projectPath`. Disabled with `EV_AI_HOOK_USER_SPEC=off`.
25
- - **User spec (project-dir components)** — sibling snapshot of the same component kinds from up to three extra directories on the same `session_start` POST: the launch working directory (`origin: cwd`), the git toplevel when `remote.origin` is set (`origin: repo`), and at most one covering parent above that git root (`origin: parent`). Equal resolved paths are not walked twice (a session launched at the git root skips `cwd`; a git root that is home skips the project walk). Intermediate ancestors between cwd and git root, and descendants below the launch dir, are not collected. Plugins and the admin-managed tier stay home-only. Gitignored project files (`settings.local.json`, `CLAUDE.local.md`, uncommitted / locally edited spec) **are** collected — they are the reason this walk exists. A personal clone opened in the same IDE is inventoried under the org (cwd walk always; repo walk when the clone has an origin remote); a covering parent such as `~/dev/CLAUDE.md` above both work and personal clones is collected when present. Wire `path` is `~/dev/acme/.claude/…` when the file is under home, POSIX absolute when it is not (no `cwd/` prefix; home-scope rows stay `.claude/…` with no `~/`). Same per-component delta, text-once-per-hash, and shared byte budget as home; each project directory has its own 14-hour cooldown so a first visit to a nested dir can send cwd full while home stays incremental. Envelope fields: `user_spec_scopes` (which partitions this walk covered), `user_spec_cwd_scope_ref` / `user_spec_repo_scope_ref` / `user_spec_parent_scope_ref` (path hashes of those anchors — not the git remote), `user_spec_parent_rel` (`..` / `../..` from launch cwd to the covering parent), `user_spec_cwd_in_git_repo` (telemetry), `user_spec_full_scopes` / `user_spec_truncated_scopes` (per-partition authority; mixed sends never set `user_spec_full: true`). Codex project TOML is the same presence/size gap as home (no TOML reader). Disable the three project walks with `EV_AI_HOOK_CWD_SPEC=off` (home spec and operator identity keep running). `EV_AI_HOOK_USER_SPEC=off` disables every scope.
26
- - **Operator identity** — on every event (`session_start`, `session_end`, `tool_invoked`), independently of the user-spec cooldown. Existing `self_config` fields are preserved; a stale operator on that object is replaced with the freshly resolved identity. `git_user_name` / `git_user_email` are always real git (`git config`, else `GIT_AUTHOR_*` / `GIT_COMMITTER_*` / `EMAIL`). `platform_login` is the platform logged-in email when the runner has one: Claude `~/.claude.json` `oauthAccount.emailAddress` (`identity_source: "claude_oauth"`; `accountUuid` only if email is absent), Cursor `~/.cursor/cli-config.json` `authInfo.email` (`identity_source: "cursor_oauth"`; `authId` / `userId` only if email is absent; IDE-only seats without that CLI file use `cursorAuth/*` rows in the desktop app's `state.vscdb`, opened read-only/immutable so a live Cursor cannot lock the hook — `cachedEmail`, else `stripeMembershipAuthId`; access and refresh tokens are never selected; the parsed result — the email, the auth-id fallback, or the fact that the db holds neither — is cached at `~/.ev-ai/agent-hook/cursor-ide-identity.json` and only re-read when that cache has never been written, was written against a different `state.vscdb` path, or is older than the same 14-hour cooldown the user-spec home walk uses, so identity on every event does not reopen a multi-GB sqlite file each time; the cache holds no tokens), Codex ChatGPT email from `CODEX_HOME/auth.json` (`identity_source: "codex_oauth"`). Both git and platform fields are sent together when both are known. Codex API keys, raw tokens, refresh tokens, plan data, and ChatGPT workspace/account ids are never emitted; API-key, keychain-only, absent, malformed, or incomplete Codex credentials continue through the normal fallbacks. A bot-shaped runner login is skipped so CI actor env vars can win `platform_login`. Bot-shaped git identity (`*[bot]*`, `action@github.com`, `noreply@anthropic.com`, `cursoragent@cursor.com`, …) is skipped so CI/cloud actor env vars can win `platform_login`: explicit `EV_AI_HOOK_OPERATOR_EMAIL` / `EV_AI_HOOK_OPERATOR_LOGIN` (and name) → `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"`. CI/cloud actor sources set `platform_login` to the platform email when they expose one, else the actor login. 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 `platform_login` from `CURSOR_USER_EMAIL` when it is not the image-baked bot address (never a session uuid). Never invents an OS username. Disabled with the same `EV_AI_HOOK_USER_SPEC=off` opt-out.
24
+ - **User spec (home-dir components)** — per-component delta of skills / commands / rules / memory / subagents / hooks / settings / MCP servers / plugins from `~/.claude`, `~/.cursor`, `~/.codex`, `~/.agents`, and from the home root itself (`~/CLAUDE.md`, `~/AGENTS.md` — Claude's and Codex's memory walk reaches them). Hook rows are parsed out of every settings file each runner reads, not just the dedicated hooks file: Claude `settings.json` / `settings.local.json` / `hooks.json`, Cursor `hooks.json` / `sandbox.json` / `permissions.json` / `cli-config.json`, Codex `hooks.json` — plus the admin-managed layer (Claude `managed-settings.json` with its `managed-settings.d/*.json` drop-ins **and** `~/.claude/remote-settings.json`, the cached payload from the claude.ai console; Cursor enterprise `hooks.json` and cloud-synced team hooks under `~/.cursor/managed/<team>/`). Admin-managed sources are org-written rather than operator-written, so they are reported as that tier even when the file itself sits under home. Allowlisted sandbox / permissions / MCP-toggle keys from those JSON files also ship as `SETTINGS` (operator-written) or `MANAGED_SETTINGS` (admin-written) rows: one row per existing file, `name: "settings"`, nested `attributes.settings` (same shape as the parsed object, not a JSON string), plus `keys_omitted` (top-level unread key **names** only — values never leave the machine). `hooks{}` is excluded from `settings` because it already ships as HOOK rows (the name still appears in `keys_omitted`). Never shipped as values: Claude `env`, `apiKeyHelper`, `awsAuthRefresh`, `awsCredentialExport`, `otelHeadersHelper`; Cursor `authInfo`. A missing file produces no row; a file that exists but is not a valid JSON object (including JSONC) produces a row with `parse_error: true`, and one that exists but cannot be read (e.g. permission denied) produces `read_error: true` — neither carries `settings`. A leading UTF-8 byte-order mark (written by Windows editors and PowerShell 5) is ignored. Claude allowlist: `permissions` (whole object), `sandbox` (whole object), `enableAllProjectMcpServers`, `enabledMcpjsonServers`, `disabledMcpjsonServers`, `disableAllHooks`, `disableAutoMode`, and the managed-only locks `allowManagedHooksOnly`, `allowManagedPermissionRulesOnly`, `allowManagedMcpServersOnly`, `strictKnownMarketplaces`, `strictPluginOnlyCustomization`, `disableSideloadFlags`, `forceLoginMethod`, `forceLoginOrgUUID`. Cursor `sandbox.json`: `type`, `additionalReadwritePaths`, `additionalReadonlyPaths`, `disableTmpWrite`, `enableSharedBuildCache`, `networkPolicy`. Cursor `permissions.json`: `terminalAllowlist`, `mcpAllowlist`, `autoRun`. Cursor CLI (`cli-config.json` at home, `cli.json` at a project): `permissions` only. Permission rule strings (Claude `permissions.allow|deny|ask`, Cursor CLI tokens, `terminalAllowlist`, `sandbox.excludedCommands`, `mcpAllowlist`) have absolute paths rewritten to `<path>`; filesystem path lists (`sandbox.filesystem.*`, `sandbox.credentials.files[].path`, `permissions.additionalDirectories`, Cursor `additionalReadwritePaths` / `additionalReadonlyPaths`) rewrite the home prefix to `~` without collapsing the rest of the path (separators normalized to `/`; a Windows home such as `C:\Users\me` matches case-insensitively), and so do Unix socket paths in `sandbox.network.allowUnixSockets`. Every other string, rule strings included, also has the home directory replaced with `~` wherever it appears, with either separator, so no allowlisted value carries the OS username. Arrays are capped at 200 entries, strings at 200 characters, nesting at 6 levels, and serialized `settings` at 16 KiB: over the byte cap the longest lists lose trailing entries first, and a top-level key is dropped only when no list is left to shorten. When any cap hits the row carries `truncated: true` and `trimmed_paths` (the dotted paths shortened or removed). If the collector still cannot store `settings` it keeps the row and sets `settings_dropped: true`. Claude managed files add `managed_source` (`server` / `file` / `file_dropin`). Codex TOML settings (`config.toml`, `managed_config.toml`, `requirements.toml`) are presence/size only apart from their `[mcp_servers]` sections. Not collected: `$CLAUDE_CONFIG_DIR` (no walker honours it today); Claude managed policy delivered only by MDM plist / Windows registry (not a readable file path); Cursor team-admin sandbox/permissions policy (dashboard-side, not on disk). Project-directory spec is a sibling snapshot (next bullet), not omitted from this envelope. Walked on every `session_start` (`user_spec_collected_at_ms` in `self-config-cache.json` records the last successful walk). A session start on which the launcher also runs its background update check is collected by that check once it finishes (at most 10 s later), on whatever version it selected, so its user-spec walk runs as usual. Each upsert row carries `kind`, path-derived `name`, home-relative `path`, capped file `text` (≤100 KiB) 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 → 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 2 MiB user-spec byte budget, overflowing bodies are deferred (`user_spec_text_deferred`) before rows are dropped. Self-config caches commit only after a successful collector POST. Operator identity is attached on the POST itself (every event), independently of the user-spec walk. Hook rows send `action_kind` + `action_hash` plus a sanitized `command` (home directory rewritten to `~`, URLs/tokens/secret-shaped args redacted, capped at 300 chars) — never the raw unsanitized string. Prompt-type hook actions send only `action_kind` + `action_hash`. MCP rows send name, `transport`, a credential-stripped `command_or_url` (from `command` / `url` / `serverUrl`), redacted `args`, env var **names** (from `env` / `envVars` / `env_vars` / `bearer_token_env_var` / `env_http_headers` values), `headers_present`, `env_http_header_names`, `auth`, `bearer_token_env_var`, `default_tools_approval_mode`, declared tool allow/deny lists (`tools`, `auto_approve`, `always_allow`, `enabled_tools`, `disabled_tools` — tool names verbatim, boolean `auto_approve` when not a list, `<attr>_truncated` when a list is capped), and — for a server launched via `npx` / `npm exec` — `package_name` / `package_spec` / `package_version` / `package_version_source` resolved from npm's `_npx` install tree exactly as on `tool_invoked` above (so a floating `@latest` row's `content_hash` moves when the installed version changes, which is the point: the row re-sends as new evidence) — never env or header values; a server switched off in config (`enabled: false` / `disabled: true`) is reported with the row's `enabled` flag false rather than as live surface. Every other declared config key is surfaced by name with its value replaced by `<omitted>`, so the config shape is visible without its contents. Plugin rows never include `installPath` / `projectPath`. Disabled with `EV_AI_HOOK_USER_SPEC=off`.
25
+ - **User spec (project-dir components)** — sibling snapshot of the same component kinds from up to three extra directories on the same `session_start` POST: the launch working directory (`origin: cwd`), the git toplevel when `remote.origin` is set (`origin: repo`), and at most one covering parent above that git root (`origin: parent`). Equal resolved paths are not walked twice (a session launched at the git root skips `cwd`; a git root that is home skips the project walk). Intermediate ancestors between cwd and git root, and descendants below the launch dir, are not collected. Plugins and the admin-managed tier stay home-only. Gitignored project files (`settings.local.json`, `CLAUDE.local.md`, uncommitted / locally edited spec) **are** collected — they are the reason this walk exists. A personal clone opened in the same IDE is inventoried under the org (cwd walk always; repo walk when the clone has an origin remote); a covering parent such as `~/dev/CLAUDE.md` above both work and personal clones is collected when present. Wire `path` is `~/dev/acme/.claude/…` when the file is under home, POSIX absolute when it is not (no `cwd/` prefix; home-scope rows stay `.claude/…` with no `~/`). Same per-component delta, text-once-per-hash, and shared byte budget as home; each project directory is walked independently so a first visit to a nested dir can send cwd full while home stays incremental. Envelope fields: `user_spec_scopes` (which partitions this walk covered), `user_spec_cwd_scope_ref` / `user_spec_repo_scope_ref` / `user_spec_parent_scope_ref` (path hashes of those anchors — not the git remote), `user_spec_parent_rel` (`..` / `../..` from launch cwd to the covering parent), `user_spec_cwd_in_git_repo` (telemetry), `user_spec_full_scopes` / `user_spec_truncated_scopes` (per-partition authority; mixed sends never set `user_spec_full: true`). Codex project TOML is the same presence/size gap as home (no TOML reader). Disable the three project walks with `EV_AI_HOOK_CWD_SPEC=off` (home spec and operator identity keep running). `EV_AI_HOOK_USER_SPEC=off` disables every scope.
26
+ - **Operator identity** — on every event (`session_start`, `session_end`, `tool_invoked`), independently of the user-spec walk. Existing `self_config` fields are preserved; a stale operator on that object is replaced with the freshly resolved identity. `git_user_name` / `git_user_email` are always real git (`git config`, else `GIT_AUTHOR_*` / `GIT_COMMITTER_*` / `EMAIL`). `platform_login` is the platform logged-in email when the runner has one: Claude `~/.claude.json` `oauthAccount.emailAddress` (`identity_source: "claude_oauth"`; `accountUuid` only if email is absent), Cursor `~/.cursor/cli-config.json` `authInfo.email` (`identity_source: "cursor_oauth"`; `authId` / `userId` only if email is absent; IDE-only seats without that CLI file use `cursorAuth/*` rows in the desktop app's `state.vscdb`, opened read-only/immutable so a live Cursor cannot lock the hook — `cachedEmail`, else `stripeMembershipAuthId`; access and refresh tokens are never selected; the parsed result — the email, the auth-id fallback, or the fact that the db holds neither — is cached at `~/.ev-ai/agent-hook/cursor-ide-identity.json` and only re-read when that cache has never been written, was written against a different `state.vscdb` path, or is older than the 14-hour Cursor IDE identity cache TTL, so identity on every event does not reopen a multi-GB sqlite file each time; the cache holds no tokens), Codex ChatGPT email from `CODEX_HOME/auth.json` (`identity_source: "codex_oauth"`). Both git and platform fields are sent together when both are known. Codex API keys, raw tokens, refresh tokens, plan data, and ChatGPT workspace/account ids are never emitted; API-key, keychain-only, absent, malformed, or incomplete Codex credentials continue through the normal fallbacks. A bot-shaped runner login is skipped so CI actor env vars can win `platform_login`. Bot-shaped git identity (`*[bot]*`, `action@github.com`, `noreply@anthropic.com`, `cursoragent@cursor.com`, …) is skipped so CI/cloud actor env vars can win `platform_login`: explicit `EV_AI_HOOK_OPERATOR_EMAIL` / `EV_AI_HOOK_OPERATOR_LOGIN` (and name) → `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"`. CI/cloud actor sources set `platform_login` to the platform email when they expose one, else the actor login. 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 `platform_login` from `CURSOR_USER_EMAIL` when it is not the image-baked bot address (never a session uuid). Never invents an OS username. Disabled with the same `EV_AI_HOOK_USER_SPEC=off` opt-out.
27
27
  - **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`.
28
28
  - **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. Merged across the git-invisible tiers: admin-managed settings (server-managed payload, else `managed-settings.json` + drop-ins), user settings, and the untracked project override. Managed rules are listed first because they cannot be overridden and so must not be what the cap discards; repeat `key`+`mode` pairs are sent once. Policy delivered only by MDM plist or Windows registry is not a readable file path and stays uncollected. 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.
29
29
  - **Allowlisted hook metadata** — see below (includes SessionStart `model`, `permission_mode`, …)
@@ -50,7 +50,7 @@ Every event also carries: UTC timestamp, `installation_id` (per-machine under `~
50
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.
51
51
  - `duration_ms` — from Claude `duration_ms` or Cursor `duration` on post-tool / `afterShellExecution` stdin
52
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.
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
+ - **`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 exact version behind it. `mcp_package_version` is read from npm's own `_npx` install tree — the directory npm names from a hash of the package specs — when that tree exists, and is then what really ran (`mcp_package_version_source: npx_cache`). When there is no tree and the spec names an exact version (`gitnexus@1.6.5`), that pin is reported with `mcp_package_version_source: declared_pin`, so a declared pin is never mistaken for an observed install; an installed version always wins over the pin. A floating tag reports its resolved version; a cache miss on a floating tag or range 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.
54
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.
55
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
56
56
  - Allowlisted hook metadata (see below)
@@ -59,19 +59,25 @@ Every event also carries: UTC timestamp, `installation_id` (per-machine under `~
59
59
 
60
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.
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).
62
+ **Content fields (sent unless killed):** `tool_input`, `tool_response`, `last_assistant_message`, and Cursor top-level `command`. `prompt` is **opt-in**: it joins them only with `EV_AI_HOOK_CAPTURE_PROMPT=on` (the kill switch below still wins). 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
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).
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` / `SubagentStart` / `SubagentStop`. 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).
65
65
 
66
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).
67
67
 
68
+ Every envelope also carries `policy_version` (the integer in the local cache, or `0` when none) and `policy_capabilities` (selectors this hook build can evaluate). The collector may reply with a rule table when that version is stale. `install` POSTs a lightweight `session_start` (`run_meta.trigger: policy_install_fetch`) so the first table is on disk before a session starts.
69
+
70
+ ## Local policy cache (inbound)
71
+
72
+ The hook caches your organisation's policy rules locally at `~/.ev-ai/agent-hook/policy.json`. That file is written only from the collector's response to hook events the hook already sends — not from a repo manifest or `~/.ev-ai/config.json`. Beside it, `policy-confirmed.json` records the policy version and the time the collector last confirmed it is current (from an `x-ev-policy-version` response header); past the policy's `max_age_ms` without a confirmation, findings are recorded as `policy_stale` and enforcement is off. The table may include `enforcement_mode`. In `report_only` (the organisation default), and for any rule whose action is `report`, a match is recorded as a finding and is not used to rewrite the tool call, prompt you, or block it; the hook also does not spawn a classifier for those matches. Rule evaluation happens on your machine. Prompts and tool inputs are matched against those rules locally; a match does not send anything additional. For rules that need judgement and that the organisation is enforcing, the hook invokes your own installed coding CLI (Claude / Cursor / Codex) with the text being evaluated, using your existing login. The hook does not send policy text to any additional network destination, does not call a model of ours, and does not interpret policy prose on the endpoint.
73
+
68
74
  ## PII we do not send
69
75
 
70
76
  Username and hostname *are* sent (per-member usage for the installing org). The following are not:
71
77
 
72
78
  - Chat transcripts (the JSONL file), session titles, and keystrokes
73
79
  - 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`
80
+ - User **prompts** — dropped unless `EV_AI_HOOK_CAPTURE_PROMPT=on` (or full stdin capture). The last assistant message **is** sent unless `EV_AI_HOOK_CAPTURE_CONTENT=off`
75
81
  - 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`
76
82
  - 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
77
83
  - 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,8 @@ 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 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).
11
+ - **Prompt collection is opt-in.** User prompt bodies (`hook_input.prompt`) are dropped by default; set `EV_AI_HOOK_CAPTURE_PROMPT=on` to send them. Policy rules still match prompts locally either way.
12
+ - **Collection is opt-out for tool-arg / tool-result / last-assistant-message bodies.** Those three `hook_input` fields are sent by default (size-capped). Set `EV_AI_HOOK_CAPTURE_CONTENT=off` or `capture_hook_input: "allowlist"` to drop them (and the prompt, even when `EV_AI_HOOK_CAPTURE_PROMPT=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).
12
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 does forward the bodies of tool calls the agent makes, unless content capture is turned off.
13
14
 
14
15
  ## What this package does on your machine
@@ -38,7 +39,7 @@ A plain list of every behavior a security reviewer or supply-chain scanner will
38
39
 
39
40
  **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.
40
41
 
41
- **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](#privacy-defaults).
42
+ **Content capture.** 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`. Prompt bodies are off by default; turn them on with `EV_AI_HOOK_CAPTURE_PROMPT=on`. See [Privacy defaults](#privacy-defaults).
42
43
 
43
44
  **Shipped code is bundled and minified** (`dist/*.mjs`, built with esbuild from TypeScript).
44
45
 
@@ -169,7 +170,7 @@ The npx package's entry point is a small launcher. It runs the hook your collect
169
170
 
170
171
  **Network.** Besides the collector POST, the launcher makes two kinds of request, both only on the npx path:
171
172
 
172
- - `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.
173
+ - `GET /{orgToken}/version` on the same collector, at most once per 30 min 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.
173
174
  - 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.
174
175
 
175
176
  **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.
@@ -201,7 +202,7 @@ Debug envelope logging: `EV_AI_HOOK_DEBUG=1` (redacted summary). Full envelope:
201
202
 
202
203
  Full field-by-field inventory of what is POSTed: [COLLECTED-DATA.md](./COLLECTED-DATA.md).
203
204
 
204
- - **`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.
205
+ - **`hook_input`**: metadata (session/tool ids, status, …) plus content fields by default — `tool_input`, `tool_response`, `last_assistant_message`, and Cursor `command`. `prompt` is opt-in: `EV_AI_HOOK_CAPTURE_PROMPT=on` (also accepts `1` / `true`). 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.
205
206
  - **`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.
206
207
  - **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).
207
208
  - **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`.