@ev-ai/agent-hook 1.0.10 → 1.1.1

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
@@ -19,13 +19,14 @@ Every event also carries: UTC timestamp, `installation_id` (per-machine under `~
19
19
  - **Host** — hostname, OS / version, username, shell, TTY, runtime context (`interactive` / `github_actions` / `claude_remote` / `cursor_cloud` / `codespaces` / `gitlab_ci` / `managed` / `container` / `unknown`). Cloud / CI contexts require positive evidence (`GITHUB_ACTIONS`, `CLAUDE_CODE_REMOTE`, `CODESPACES`, `GITLAB_CI`, managed CI platform markers, truthy `CI`, or Cursor Cloud). Cursor Cloud is `CURSOR_AGENT` without a local IDE host, a `bc-` background-composer session id on Linux, or hostname `cursor` + os user `ubuntu` + `CURSOR_VERSION` on Linux. `CURSOR_AGENT` / those extra markers are not enough when a local Cursor / VS Code extension host is present (desktop agent shells set `CURSOR_AGENT` too). `CURSOR_CODE_REMOTE` alone is not Cursor Cloud (Remote-SSH boxes also set it). `gitlab_ci` is `GITLAB_CI`. `managed` covers generic `CI=true` plus named platforms that often omit it (`TF_BUILD`, `JENKINS_URL`, `BUILDKITE`, `CIRCLECI`, `CODEBUILD_BUILD_ID`, `DRONE`, …). `container` covers `/.dockerenv`, k8s (`KUBERNETES_SERVICE_HOST`), and VS Code / Codespaces-style remote containers (`REMOTE_CONTAINERS` / `DEVCONTAINER`). `interactive` covers TTY / `TERM` / `TERM_PROGRAM`, SSH session markers, macOS GUI launch (`__CFBundleIdentifier`, `XPC_SERVICE_NAME`), IDE host markers (`VSCODE_PID`, …), and Linux / Windows desktop session vars. Residual with no positive cloud or local evidence is `unknown` (the former `cron` catch-all for “no TTY” was removed — GUI-launched agents often have no TTY). Optional virt and cloud provider / region / instance id. Cloud metadata is probed only for non-interactive contexts; interactive laptop sessions skip IMDS entirely. Non-interactive probes are cached locally for 24h per context.
20
20
  - **Git** — origin remote (credentials stripped), branch, HEAD SHA, dirty flag
21
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
- - **MCP inventory** — declared server **names** (and discovery source), not commands/URLs/env
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.
24
- - **User spec (home-dir components)** — per-component delta of skills / commands / rules / memory / subagents / hooks / 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` / `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. Only the `hooks{}` object is read from those files; the rest of their contents (auth blobs, caches, permissions) is never shipped as `text`. Codex TOML settings (`config.toml`, `managed_config.toml`, `requirements.toml`) are presence/size only apart from their `[mcp_servers]` sections. 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*` / `operator`. 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 / operator caches commit only after a successful collector POST. 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`, and 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) — 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`.
24
+ - **User spec (home-dir components)** — per-component delta of skills / commands / rules / memory / subagents / hooks / 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` / `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. Only the `hooks{}` object is read from those files; the rest of their contents (auth blobs, caches, permissions) is never shipped as `text`. Codex TOML settings (`config.toml`, `managed_config.toml`, `requirements.toml`) are presence/size only apart from their `[mcp_servers]` sections. 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*` / `operator`. 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 / operator caches commit only after a successful collector POST. 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
- - **Operator identity** — on the first user-spec collect for an installation (and again when the 14-hour user-spec cooldown elapses): `git_user_name` / `git_user_email` from `git config` when set (`identity_source: "git_config"`). When unset, tries `GIT_AUTHOR_NAME`/`GIT_AUTHOR_EMAIL`, then `GIT_COMMITTER_*`, then `EMAIL` (`identity_source: "git_env"`). Bot-shaped git identity (`*[bot]*`, `action@github.com`, `noreply@anthropic.com`, `cursoragent@cursor.com`, …) is skipped so CI/cloud actor env vars can win: explicit `EV_AI_HOOK_OPERATOR_NAME` / `EV_AI_HOOK_OPERATOR_EMAIL` / `EV_AI_HOOK_OPERATOR_LOGIN` → `identity_source: "configured"`; then `GITHUB_ACTOR` / `GITHUB_TRIGGERING_ACTOR` / `GITHUB_USER` → `"github"`; `GITLAB_USER_*` → `"gitlab"`; Azure DevOps `BUILD_REQUESTEDFOR*` → `"azure_devops"`; Buildkite `BUILDKITE_BUILD_CREATOR*` → `"buildkite"`; Jenkins `BUILD_USER*` / `CHANGE_AUTHOR*` → `"jenkins"`; CircleCI `CIRCLE_USERNAME` → `"circleci"`; CodeBuild `CODEBUILD_INITIATOR` → `"codebuild"`; Drone/Woodpecker `DRONE_COMMIT_AUTHOR*` → `"drone"`. When those are also absent, operator name/email stay unset — HEAD commit author is not used. Claude Code remote (`CLAUDE_CODE_REMOTE`) and Cursor cloud (`CURSOR_AGENT`) without a git or CI actor are tagged `identity_source: "cloud"`; Cursor cloud sets `git_user_email` from `CURSOR_USER_EMAIL` when it is not the image-baked bot address (never a session uuid). CI/cloud actor sources also set `platform_login` (GitHub/GitLab/CircleCI/CodeBuild/Jenkins `BUILD_USER_ID`/Drone login, Azure DevOps `BUILD_REQUESTEDFORID`, Buildkite creator email, or `EV_AI_HOOK_OPERATOR_LOGIN`) so the collector can join installations to members without inventing a noreply email. Omitted while user-spec is on cooldown and in steady state when unchanged. Never invents an OS username. Disabled with the same `EV_AI_HOOK_USER_SPEC=off` opt-out.
26
+ - **Operator identity** — on the first user-spec collect for an installation (and again when the 14-hour user-spec cooldown elapses): when `--runner claude`, the Claude Code login in `~/.claude.json` (`oauthAccount`) outranks git (`identity_source: "claude_oauth"`, `platform_login` is the account uuid); when `--runner cursor`, the Cursor login in `~/.cursor/cli-config.json` (`authInfo`) outranks git (`identity_source: "cursor_oauth"`, `platform_login` is the auth id); when `--runner codex`, a file-backed ChatGPT login in `CODEX_HOME/auth.json` (default `~/.codex/auth.json`) outranks git (`identity_source: "codex_oauth"`, `platform_login` is the human ChatGPT user id decoded locally from the ID token). 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. Otherwise `git_user_name` / `git_user_email` from `git config` when set (`identity_source: "git_config"`). When unset, tries `GIT_AUTHOR_NAME`/`GIT_AUTHOR_EMAIL`, then `GIT_COMMITTER_*`, then `EMAIL` (`identity_source: "git_env"`). Bot-shaped git identity (`*[bot]*`, `action@github.com`, `noreply@anthropic.com`, `cursoragent@cursor.com`, …) is skipped so CI/cloud actor env vars can win: explicit `EV_AI_HOOK_OPERATOR_NAME` / `EV_AI_HOOK_OPERATOR_EMAIL` / `EV_AI_HOOK_OPERATOR_LOGIN` → `identity_source: "configured"`; then `GITHUB_ACTOR` / `GITHUB_TRIGGERING_ACTOR` / `GITHUB_USER` → `"github"`; `GITLAB_USER_*` → `"gitlab"`; Azure DevOps `BUILD_REQUESTEDFOR*` → `"azure_devops"`; Buildkite `BUILDKITE_BUILD_CREATOR*` → `"buildkite"`; Jenkins `BUILD_USER*` / `CHANGE_AUTHOR*` → `"jenkins"`; CircleCI `CIRCLE_USERNAME` → `"circleci"`; CodeBuild `CODEBUILD_INITIATOR` → `"codebuild"`; Drone/Woodpecker `DRONE_COMMIT_AUTHOR*` → `"drone"`. When those are also absent, operator name/email stay unset — HEAD commit author is not used. Claude Code remote (`CLAUDE_CODE_REMOTE`) and Cursor cloud (`CURSOR_AGENT`) without a git or CI actor are tagged `identity_source: "cloud"`; Cursor cloud sets `git_user_email` from `CURSOR_USER_EMAIL` when it is not the image-baked bot address (never a session uuid). CI/cloud actor sources also set `platform_login` (GitHub/GitLab/CircleCI/CodeBuild/Jenkins `BUILD_USER_ID`/Drone login, Azure DevOps `BUILD_REQUESTEDFORID`, Buildkite creator email, or `EV_AI_HOOK_OPERATOR_LOGIN`) so the collector can join installations to members without inventing a noreply email. Omitted while user-spec is on cooldown and in steady state when unchanged. Never invents an OS username. Disabled with the same `EV_AI_HOOK_USER_SPEC=off` opt-out.
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
+ - **Codex per-event operator identity** — the user-spec cadence above does not apply to a usable file-backed ChatGPT login. Every event emitted with `--runner codex` reads `CODEX_HOME/auth.json` and attaches `self_config.operator` with `identity_source: "codex_oauth"`; this is independent of user-spec cooldowns and `EV_AI_HOOK_USER_SPEC`. Existing `self_config` fields are preserved, and any stale operator from the shared installation cache is replaced. Only the human name, email, and ChatGPT user id are emitted; tokens and workspace/account ids remain excluded. When a usable ChatGPT login is unavailable, no per-event Codex operator is attached and the existing session-start fallback behavior remains unchanged.
29
30
  - **Allowlisted hook metadata** — see below (includes SessionStart `model`, `permission_mode`, …)
30
31
 
31
32
  ### On `session_end`
@@ -36,17 +37,21 @@ Every event also carries: UTC timestamp, `installation_id` (per-machine under `~
36
37
 
37
38
  ### On `tool_invoked`
38
39
 
39
- - `tool_name`, `skill_name`
40
+ - `tool_name`, `skill_name`, `rule_name`, `command_name`
40
41
  - **`executable`** — first token of a shell or MCP spawn command when stdin has one (`tool_input.command` or Cursor top-level `command`). Args, flags, and the rest of the line are never sent. Home-directory prefixes are rewritten to `~`. Omitted when the tool has no command (Read / Write / Grep / Skill / MCP tool calls).
41
42
  - **Cursor `skill_name` (derived, not from a native Skill tool):** slash commands via `beforeSubmitPrompt` (`/my-skill` → `tool_name: Skill`, `skill_name: my-skill`; prompt text is never POSTed); Read of `**/skills/<id>/SKILL.md` on `preToolUse` / `postToolUse` / `beforeReadFile` (skill id only — file path is never POSTed); Shell whose command references a skill path on `beforeShellExecution` / `afterShellExecution` (id only — the command line is never POSTed; argv0 may appear as `executable`). Silent auto-inject with no slash and no SKILL.md Read is not observable.
42
43
  - **Claude `skill_name` (derived):** slash commands via `UserPromptExpansion` (`command_name` → `tool_name: Skill`, `skill_name: <id>`; `prompt` / `command_args` are never POSTed). Typing `/skillname` bypasses `PreToolUse(Skill)`; Read of `**/skills/<id>/SKILL.md` on Pre/Post tool hooks is also derived when present.
43
44
  - **Codex `skill_name` (derived):** slash / custom-prompt ids via `UserPromptSubmit` (`/my-skill` → `tool_name: Skill`, `skill_name: my-skill`; prompt text is never POSTed); Bash/Shell whose `tool_input.command` references `.claude/.cursor/.agents/skills/<id>/SKILL.md` or `.codex/commands/<id>.md` (id only — the command line is never POSTed; argv0 may appear as `executable`); Read-like tools of those paths when present. Silent auto-inject with no slash and no skill-file touch is not observable.
45
+ - **Cursor `rule_name` (derived):** always-apply rules via `beforeSubmitPrompt` `attachments[]` (`type: "rule"` → `tool_name: Rule`; `file_path` / `attachments` / prompt text are never POSTed). 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
+ - **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.
47
+ - **`command_name` (derived, all runners):** the slash-command id when a slash invocation resolves to a command file rather than a `SKILL.md` (`.claude/commands/**`, `.cursor/commands/**`, `.codex/prompts/**`), and the path-derived id on a Read / Shell that touches one of those files. Sent alongside `skill_name` on slash events, never instead of it. Namespaced commands keep their directory prefix (`git/commit`). When the same id exists under more than one commands root, the running runner's root is tried first (Claude → `.claude/commands`, Cursor → `.cursor/commands`, Codex → `.codex/prompts`).
44
48
  - `tool_use_id` — opaque Claude Pre/Post join key when present in stdin
45
49
  - `success` — `true` on Claude/Cursor `PostToolUse` / `postToolUse`, `false` on `PostToolUseFailure` / `postToolUseFailure` / Claude `PermissionDenied`, otherwise `null` (Cursor `afterFileEdit` / `afterShellExecution` have no exit code; Post* owns outcome)
46
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.
47
51
  - `duration_ms` — from Claude `duration_ms` or Cursor `duration` on post-tool / `afterShellExecution` stdin
48
- - `mcp_server` / `mcp_tool` — derived from opaque `tool_name` when it matches Claude `mcp__<server>__<tool>` or Cursor `MCP:<server>:<tool>` / `MCP:<tool>` (no `tool_input` parsing)
49
- - `component_reference` — an opaque exact-component identifier emitted only when the hook has complete repository-backed identity evidence. Exact skill paths map to SKILL installations; exact MCP server and MCP tool invocations map to the server INTERFACE installation, never to a tool installation. Name-only, flat/unattributed MCP, malformed scope, and missing platform-agent identity stay unmatched. Absolute local paths never leave the hook process: path evidence is normalized to the repository-relative source path before hashing, and only the opaque reference is sent.
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 (tokenized in-process, never POSTed). 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.
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.
50
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
51
56
  - Allowlisted hook metadata (see below)
52
57