@ev-ai/agent-hook 1.2.6 → 1.2.7
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 +1 -1
- package/README.md +1 -1
- package/dist/cli.mjs +20 -20
- package/dist/hook.mjs +15 -15
- package/dist/launcher-update.mjs +4 -4
- package/dist/launcher.mjs +3 -3
- package/dist/managed-hooks-snippet.cjs +1 -1
- package/dist/managed-hooks-snippet.mjs +1 -1
- package/dist/plugin-hooks.mjs +1 -1
- package/package.json +1 -1
package/COLLECTED-DATA.md
CHANGED
|
@@ -21,7 +21,7 @@ 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
|
|
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
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
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.
|
|
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`.
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ A plain list of every behavior a security reviewer or supply-chain scanner will
|
|
|
25
25
|
|
|
26
26
|
**Auto-update: the package can run code it did not ship with.** On the npx path the launcher may run a newer `hook.mjs` that your collector selects, downloaded to `~/.ev-ai/agent-hook/js/signed-v1/`, verified against an ev-ai signed envelope (built-in public key) and SHA-512 of the bytes, and executed with the same Node that runs the launcher. It is on by default; turn it off with `EV_AI_AUTO_UPDATE=0` or `"auto_update": false` in `~/.ev-ai/config.json`, and the pinned, bundled hook is the only code that runs. A repo manifest cannot change this setting.
|
|
27
27
|
|
|
28
|
-
**Background processes.** To ack the runner quickly, most events are collected by a detached child process that exits after one POST. At session start the launcher may also spawn one detached `__refresh` process that performs the update check above and exits (skipped when auto-update is off).
|
|
28
|
+
**Background processes.** To ack the runner quickly, most events are collected by a detached child process that exits after one POST. At session start the launcher may also spawn one detached `__refresh` process that performs the update check above, then collects that SessionStart with the version the check selected, and exits (skipped when auto-update is off). While it is held for the check, the SessionStart's payload is kept in a `handoff-*.json` file (mode 0600) in the collector's scope directory under `~/.ev-ai/agent-hook/js/signed-v1/scopes/`, and deleted as soon as the refresh reads it.
|
|
29
29
|
|
|
30
30
|
**Files read** (identity and configuration only, never file contents from your projects):
|
|
31
31
|
|