This document is the single source of truth for building and maintaining the Cursor integration layer. It covers the hook scripts in integrations/cursor/hooks/ (source) and their deployment to .cursor/hooks/ (editor target), the hooks.json wiring, the adapter pattern, and all known limitations of Cursor's hook system.
The AIC core (anything in shared/ or mcp/src/) has zero knowledge of Cursor. All
Cursor-specific source code lives in integrations/cursor/. The .cursor/ directory is a
deployment target that Cursor reads — it is not a source directory. This distinction is not a preference — it is a structural invariant.
What this means concretely:
-
All Cursor hook and hooks.json logic lives in
integrations/cursor/. The installer (integrations/cursor/install.cjs) is standalone.mcp/src/editor-integration-dispatch.tsruns it withexecFileSyncwhen the bootstrap gate passes (see that file — not only.cursor//CURSOR_PROJECT_DIRmay open the gate). Installer resolution matches installation.md:<project>/integrations/cursor/install.cjswhen that file exists under the opened workspace root, otherwise the copy bundled inside the published@jatbas/aicpackage at package-relativeintegrations/cursor/install.cjs. Triggers: workspace roots listed (if the client supports roots) or firstaic_compilefor that project. No copy/merge logic duplicated inmcp/src/. For a one-off refresh, diagnostics, or nonstandard layouts, runnodeon the resolved installer path with cwd at the project root (see installation.md). -
The
aic_compileMCP tool is neutral. It requiresintentandprojectRoot; shipped Cursor hooks also sendeditorId,triggerSource, and oftenconversationId/modelId. It does not know who called it. The hook adapter inintegrations/cursor/hooks/translates Cursor's hook protocol into that call — that translation is integration-layer work only. Optional wire fields beyond what hooks send are listed in §2.1.
The MCP schema (mcp/src/schemas/compilation-request.ts, summarized in Implementation specification §8c) accepts optional agentic fields such as stepIndex, stepIntent, previousFiles, toolOutputs, and conversationTokens. When callers supply structured toolOutputs[].relatedFiles (repo-relative paths by convention), the pipeline merges them into heuristic boostPatterns before scoring; when the deduplicated related-path set is non-empty, a sorted NUL-separated canonical encoding of those paths is included in the cache preimage that compilation-runner hashes (Step 4 — ContextSelector).
Shipped hooks in integrations/cursor/hooks/ that invoke aic_compile do not set toolOutputs or relatedFiles (AIC-compile-context.cjs, AIC-subagent-compile.cjs, and AIC-subagent-stop.cjs). Custom integrations may forward toolOutputs when the MCP client exposes prior tool results in that shape.
- Shared utilities are welcome in
shared/only when they are genuinely editor-agnostic (abuildSessionContext()-style helper that any editor integration could use). If a utility only makes sense for Cursor, it goes inintegrations/cursor/.
Cursor: The AIC MCP server is global in Cursor (~/.cursor/mcp.json), but Cursor does not support global hooks. The hook configuration (.cursor/hooks.json) and scripts (.cursor/hooks/AIC-*.cjs) are per-project artifacts — they must exist inside each project directory.
How they get there: The MCP server resolves the installer the same way as installation.md: in-project integrations/cursor/install.cjs when present under the workspace root, otherwise the bundled copy from @jatbas/aic, then runs it when the bootstrap gate in editor-integration-dispatch.ts passes — on workspace roots listing (if supported) or on the first aic_compile for the project. For a manual refresh or when bootstrap did not run, execute node on that resolved path with cwd at the project root (see installation.md).
The installer is idempotent: it merges hooks.json and copies every script name in integrations/cursor/aic-hook-scripts.json from integrations/cursor/hooks/ to .cursor/hooks/ (currently 13 files: thirteen AIC-*.cjs). That manifest is the canonical script list.
Optional: commit hooks to the repo. Teams can commit .cursor/hooks.json and every .cursor/hooks/AIC-*.cjs script so every clone gets hooks without re-running the installer.
Verified: Cursor documents
sessionStart,beforeSubmitPrompt,preToolUse,postToolUse,beforeShellExecution,afterFileEdit,sessionEnd,stop,subagentStart, andsubagentStop(paired withsubagentStartfor Task-tool subagent lifecycle). See Cursor agent hooks and per-event sections below.
AIC's pipeline operates on CompilationRequest → CompilationResult. It does not know which editor or tool initiated the call. This is the hexagonal architecture invariant: core/pipeline has zero knowledge of callers.
The integration layer is a thin adapter that translates Cursor's hook protocol into an
aic_compile MCP call:
Cursor runtime
│
│ stdin: { session_id, generation_id, prompt, … }
▼
.cursor/hooks/AIC-<role>.cjs ← one hook process per registration
│
│ (session compile / subagent telemetry / subagent reparent) JSON-RPC: tools/call aic_compile — §2.1
▼
mcp/src/server.ts → CompilationRunner.run()
│
│ result: { compiledPrompt, … } (only for hooks that invoke MCP)
▼
same hook process
│
│ stdout: JSON — additional_context, env, permission, etc. (see §6)
▼
Cursor runtime → injects as context
All event hooks are authored in integrations/cursor/hooks/. The installer deploys them to .cursor/hooks/ per project and does not modify mcp/ or shared/ on disk — but hooks may call the MCP tool, and the server uses mcp/ and shared/ at runtime (subagent_stop reparent updates compilation_log via shared/src/storage/reparent-subagent-compilations.ts).
AIC-compile-context.cjs, AIC-subagent-compile.cjs, and AIC-subagent-stop.cjs each spawn the MCP server via execFileSync("npx", …) (argv-only — no shell parsing of paths) and JSON-RPC to call aic_compile. Every other hook is pure Node (gate, inject, tracker, blockers, telemetry).
| Cursor hook input field | How AIC uses it |
|---|---|
input.conversation_id |
→ conversationId for aic_compile (sessionStart hook and preToolUse) |
input.model |
→ modelId on aic_compile (sessionStart and preToolUse inject) |
input.generation_id |
temp file key for per-generation state (aic-gate-<id>, aic-prompt-<id>); the recency marker aic-gate-recent-<hash> is project-scoped (§7.3) |
input.prompt |
saved to temp file by beforeSubmitPrompt for gate deny message |
input.command |
inspected by beforeShellExecution for --no-verify |
input.files / input.file / input.filePath |
edited file paths, recorded by afterFileEdit |
input.reason / input.duration_ms |
session end telemetry |
CURSOR_PROJECT_DIR env var |
project root for hooks that need it |
AIC_PROJECT_ROOT env var |
injected via env: when AIC-compile-context.cjs completes a successful aic_compile (§7.1); not emitted by AIC-session-init.cjs |
AIC_CONVERSATION_ID env var |
same env: path as AIC_PROJECT_ROOT; AIC-inject-conversation-id.cjs also falls back to process.env.AIC_CONVERSATION_ID; stdin conversation_id when present |
input.agent_transcript_path |
subagentStop only — path to the subagent transcript .jsonl; child session id is the basename without .jsonl (see §7.11) |
cursor_version / input.cursor_version |
Discriminator injected by Cursor’s hook runtime — see §4.4 |
Shipped hooks use isCursorNativeHookPayload from integrations/cursor/is-cursor-native-hook-payload.cjs to tell Cursor-native invocations apart from Claude Code invocations when both runtimes can execute overlapping hook registrations (Cursor 3 invokes ~/.claude/settings.json hooks alongside .cursor/hooks.json in that configuration).
-
Cursor hooks (
integrations/cursor/hooks/AIC-*.cjs): After parsing stdin JSON, the hook exits immediately with no side effects whenisCursorNativeHookPayloadis false (process.exit(0)or empty stdout). The predicate is true whencursor_versionorinput.cursor_versionis present, or when a resolvedconversationIdexists andintegrations/shared/editor-runtime-marker.cjsreports a fresh"cursor"marker for the project. -
Claude Code hooks (
integrations/claude/hooks/aic-*.cjs): Ifcursor_versionorinput.cursor_versionis present, the hook returns immediately without callingaic_compile, session markers, or other side effects. Claude Code (standalone or extension) does not set this field.
Treat these guards as part of the integration contract — do not remove them in refactors without re-validating cross-runtime behavior.
integrations/cursor/ is the source. .cursor/ is the per-project deployment target.
Hook sources live only under integrations/cursor/; runtime handling for some aic_compile calls (including subagent_stop reparent) lives in mcp/ and shared/.
integrations/cursor/ ← SOURCE (authored here)
hooks/
AIC-session-init.cjs # sessionStart — architectural invariants (additional_context only)
AIC-compile-context.cjs # sessionStart — calls aic_compile, injects compiled context
AIC-before-submit-prewarm.cjs # beforeSubmitPrompt — prompt logging + gate prewarm
AIC-require-aic-compile.cjs # preToolUse — compile gate (enforces aic_compile with default 300s recency fallback, override via compileRecencyWindowSecs; sibling-race poll before deny; emergency bypass when both devMode and skipCompileGate are true)
AIC-inject-conversation-id.cjs # preToolUse (MCP) — injects conversationId into MCP args
AIC-post-compile-context.cjs # postToolUse (MCP) — confirmation after aic_compile
AIC-block-no-verify.cjs # beforeShellExecution (git) — blocks --no-verify
AIC-after-file-edit-tracker.cjs # afterFileEdit — records edited files to temp file
AIC-stop-quality-check.cjs # stop — ESLint + typecheck quality gate
AIC-session-end.cjs # sessionEnd — temp file cleanup + session telemetry
AIC-subagent-compile.cjs # subagentStart — aic_compile for compilation_log telemetry
AIC-subagent-stop.cjs # subagentStop — reparent compilation_log to parent conversation
AIC-subagent-start-model-id.cjs # helper: subagent_model → modelId (deployed beside hooks)
install.cjs # Installer: copies hooks, merges hooks.json
hooks.json.template # hooks.json template
.cursor/ ← DEPLOYMENT TARGET (per-project, created by install.cjs)
hooks/
AIC-*.cjs # Deployed from integrations/cursor/hooks/
hooks.json # Merged by install.cjs
rules/
AIC.mdc # Trigger rule — instructs model to call aic_compile
Cursor hooks communicate results by writing JSON to stdout. Each event has a specific schema. Getting the schema wrong causes the hook to be silently ignored or to error.
sessionStart runs two commands in order (see hooks.json.template).
AIC-session-init.cjs writes JSON with additional_context only — architectural invariant bullets and optional AIC_CONVERSATION_ID=… text for the model. It does not emit an env object.
AIC-compile-context.cjs calls aic_compile. On success it may write JSON that includes both env and additional_context:
process.stdout.write(
JSON.stringify({
env: { AIC_PROJECT_ROOT: "...", AIC_CONVERSATION_ID: "..." },
additional_context: ["...", compiledPrompt].join("\n"),
}),
);When present, the env field sets environment variables for subsequent hooks in the session via process.env. If this hook times out or exits without a successful compile response, env is not set until a later successful compile path sets it.
process.stdout.write(JSON.stringify({ continue: true }));Must always return { continue: true }. Returning { continue: false } blocks prompt submission. This hook is zero-cost — it saves the prompt to a temp file and returns immediately.
Allow:
process.stdout.write(JSON.stringify({ permission: "allow" }));Deny (with message shown to user and agent):
process.stdout.write(
JSON.stringify({
permission: "deny",
user_message: "...",
agent_message: "...",
}),
);For the AIC-inject-conversation-id.cjs hook, an updated_input field can override the tool's input arguments:
process.stdout.write(
JSON.stringify({
permission: "allow",
updated_input: { ...toolInput, conversationId: "..." },
}),
);process.stdout.write(
JSON.stringify({
additional_context:
"AIC compilation completed. Use the compiled context for your next response.",
}),
);Same schema as preToolUse §6.3.
Side-effect only hook. Writes to temp file. Returns {}.
// Block stop and submit a follow-up automatically:
process.stdout.write(
JSON.stringify({ followup_message: "Fix lint and typecheck errors..." }),
);
// Allow stop:
process.stdout.write(JSON.stringify({}));The
followup_messagecauses Cursor to auto-submit the text as a follow-up prompt, allowing the model to fix errors before the session ends. Theloop_limitfield inhooks.jsonprevents infinite retry loops.
Side-effect only. Exit 0 always. Must never block.
Same stdout contract as §6.6: write JSON.stringify({}). The hook performs a best-effort MCP call and must not block the parent session if spawn or RPC fails.
Cursor's hook system is documented at docs.cursor.com/context/rules.
AIC registers 12 hook command entries across 10 event types (some types run more than one command); 13 hook script files are copied from aic-hook-scripts.json (all thirteen AIC-*.cjs). Limitations and workarounds are per hook below.
Event: sessionStart
Why two hooks: The first hook (AIC-session-init.cjs) injects architectural invariants from the project's AIC-architect.mdc rule file — fast, no external call, always succeeds. The second hook (AIC-compile-context.cjs) calls aic_compile to inject a broad project context snapshot — may time out, fail non-fatally.
AIC-session-init.cjs:
- Reads the
## Critical reminderssection from.cursor/rules/AIC-architect.mdc(path resolved as../rules/from.cursor/hooks/; on case-sensitive volumes the filename must match the hook’s expected name) - Extracts bullet points and outputs them as
additional_context - Builds
conversationIdasconversationIdFromTranscriptPaththenresolveConversationIdFallbackfromintegrations/shared/conversation-id.cjs(transcript/direct id first; else deterministic synthetic id fromparent_conversation_id,session_id,generation_idwhen valid — see Integrations shared modules) - Injects
AIC_CONVERSATION_ID=${conversationId}into that text when a non-empty resolved id exists — not as anenvpayload
AIC-compile-context.cjs:
- Resolves
conversationIdwith the same transcript/direct → synthetic fallback chain asAIC-session-init.cjs→ passes toaic_compilewhen non-empty - Calls
aic_compilewith intent"understand project structure, architecture, and recent changes" - On success, outputs
additional_contextwith the compiled project snapshot andenvwithAIC_PROJECT_ROOTandAIC_CONVERSATION_IDfor downstream hooks - If this hook times out (20s), it exits 0 silently — session creation is never blocked
Known limitation — aic_compile from sessionStart is best-effort: The compiled context from AIC-compile-context.cjs is broad and intent-agnostic (project structure only). The primary per-intent context delivery in Cursor relies on the model calling aic_compile itself (enforced by the preToolUse gate — see §7.3; the gate is always active unless the emergency bypass is enabled). There is no per-prompt context injection hook in Cursor — beforeSubmitPrompt does not support additional_context output (see §7.2).
File: .cursor/hooks/AIC-compile-context.cjs
Event: beforeSubmitPrompt
Input fields used:
input.prompt→ saved toos.tmpdir()/aic-prompt-<generation_id>for gate deny messageinput.generation_id→ temp file keyinput.conversation_id→ logged to.aic/prompt-log.jsonlinput.model→ logged to.aic/prompt-log.jsonl
Purpose: This hook has two jobs:
-
Prompt log: Appends one JSON line per user message to
.aic/prompt-log.jsonl(conversationId,generationId, first 200 chars astitle,model,timestamp). Age-based pruning of that file is not performed inside this hook; it runs when the AIC MCP server process starts, viashared/src/maintenance/prune-jsonl-by-timestamp.ts(same helper as.aic/session-log.jsonland.aic/session-models.jsonl). -
Gate prewarm: Writes the full
prompttext to a per-generation temp file underos.tmpdir()(aic-prompt-<generation_id>) soAIC-require-aic-compile.cjscan include the exact intent in its deny message when the enforcement path runs (§7.3). On POSIX, new files use owner-only mode0o600. Without this, the deny message falls back to a generic placeholder, reducing the chance the model uses the correct intent foraic_compile. When the emergency bypass is active (devMode+skipCompileGateboth true inaic.config.json), §7.3 returns allow before deny logic, so the exact-intent deny path does not run.
Always returns { continue: true } — this hook must never block prompt submission.
Known limitation — no context injection: beforeSubmitPrompt does not support an
additional_context output field in Cursor's current hook schema. The schema allows allow/block for the submission in general; AIC's hook always returns allow here (see above).
File: .cursor/hooks/AIC-before-submit-prewarm.cjs
Event: preToolUse (no matcher — Cursor invokes this hook for every tool call; the script may return allow immediately without reading stdin.)
Emergency bypass: Before stdin handling, the script resolves the project root via integrations/shared/resolve-project-root.cjs, reads aic.config.json, and JSON.parses it. The gate is bypassed only when the config is a plain object with both devMode === true and skipCompileGate === true. devMode alone does not bypass the gate. Any read or parse failure continues to the enforcement path below. The skipCompileGate key is intended for emergencies only (broken MCP server that must be fixed without the gate blocking tool calls) and should be removed immediately after the issue is resolved.
Input fields used (enforcement path only):
input.generation_id→ per-generation state file keyinput.tool_name→ to detect if the call isaic_compileinput.tool_input→ alternative detection foraic_compile
Purpose (enforcement path): Ensure aic_compile runs before other tools. The gate combines per-generation state tracking, a project-scoped recency window (default 300 seconds from RECENCY_WINDOW_MS in integrations/shared/compile-recency.cjs; override with positive compileRecencyWindowSecs in aic.config.json), and a short sibling-race poll before any deny verdict. There is no deny-count cap: the hook keeps denying until aic_compile runs or recency or the per-generation marker allows the call.
Rationale (sibling poll, no cap): A deny-count cap used to exist to break apparent infinite-deny loops. Those loops came from a race between sibling hook invocations in the same parallel tool batch: one invocation admits aic_compile and writes state and recency while another tool's hook reads before that write is visible and denies. Polling for up to 500 ms at 20 ms intervals re-checks the state file and recency so the batch resolves deterministically. With that behavior, the cap became unnecessary and harmful because it could silently allow tools while context was still uncompiled.
Mechanics (enforcement path):
- When the tool call is
aic_compile: writeos.tmpdir()/aic-gate-<generation_id>, update the project-scoped recency markeros.tmpdir()/aic-gate-recent-<project_hash>with the current timestamp, and allow the call. - On any other tool call, evaluate in order:
- Per-generation marker: if
aic-gate-<generation_id>exists, allow. - Recency fallback: if
aic-gate-recent-<project_hash>exists and its timestamp is within the window fromintegrations/shared/compile-recency.cjs(default 300 seconds unlesscompileRecencyWindowSecsinaic.config.jsonoverrides), allow. This coversgeneration_idchanges within the same project. - Sibling-race poll: before denying, wait 20 ms between checks, up to 500 ms total (
SIBLING_POLL_INTERVAL_MS/SIBLING_POLL_TOTAL_MSinAIC-require-aic-compile.cjs), re-checking the per-generation state file and project-scoped recency after each wait. If a siblingaic_compilein the same parallel batch writes state or recency during the poll, allow. - Deny: otherwise deny. Every later tool call repeats this flow until compile or markers allow it.
- Per-generation marker: if
- The deny message includes the exact user prompt (from the prewarm temp file) as the
recommended
intentargument so the model callsaic_compilewith the correct intent and gets a cache hit from the prewarm.
Output: { permission: "allow" } or { permission: "deny", user_message, agent_message }. Under the emergency bypass (devMode + skipCompileGate), the hook emits allow only and never deny.
failClosed behavior: The hooks.json entry has "failClosed": true — if this hook crashes or times out, Cursor denies the tool call (fail-closed). The gate is strict enforcement, not a reminder. If the gate itself has a bug, use the emergency bypass ("devMode": true + "skipCompileGate": true in aic.config.json) to unblock work while fixing the hook.
Temp file cleanup: On each invocation the hook runs an opportunistic sweep, throttled to once per 10 minutes via os.tmpdir()/aic-gate-cleanup-marker. It deletes aic-gate-* and aic-prompt-* files older than 10 minutes, excluding aic-gate-recent-* (per-project singletons) and the cleanup marker itself.
File: .cursor/hooks/AIC-require-aic-compile.cjs
Event: preToolUse with matcher: "MCP"
Input fields used:
input.conversation_id→ preferred sourceinput.conversationId→ alternate camelCase field (same role asconversation_id)AIC_CONVERSATION_IDenv var → fallback whenAIC-compile-contexthas setenvafter a successful compile, elseprocess.envmay be unset until theninput.generation_id→ key for the prewarmed prompt temp fileaic-prompt-<generation_id>(see purpose below)input.tool_name→ to scope injection toaic_compileandaic_chat_summaryinput.tool_input→ the arguments object to augment
Purpose: Cursor does not pass a conversationId in aic_compile tool arguments automatically. This hook intercepts every MCP tool call, checks if it is aic_compile or aic_chat_summary, and injects conversationId into updated_input so the MCP server receives it. Other MCP tools (aic_status, aic_last, aic_inspect, aic_projects, aic_quality_report, aic_model_test, aic_compile_spec, and any future additions) pass through unchanged — only aic_compile and aic_chat_summary are augmented here. For aic_compile, when input.generation_id is present and aic-prompt-<generation_id> in the temp directory contains non-empty text after the same normalization as the compile gate, a weak tool intent (including the MCP omitted-intent default string) may be replaced with that text (truncated) before the request reaches the server.
Output:
Resolution order for the conversation id string: input.conversation_id ?? input.conversationId ?? process.env.AIC_CONVERSATION_ID; when still unset, resolveConversationIdFallback on the hook payload supplies a synthetic id (parent_conversation_id → session_id → generation_id / camelCase, validated — see Integrations shared modules).
For aic_chat_summary, when a non-empty id is resolved, updated_input adds conversationId. When the id is missing, the hook allows the call unchanged.
For aic_compile, the hook builds an augmented toolInput with editorId: "cursor" and adds conversationId, modelId, or a replaced intent when resolution or weak-intent replacement supplies them (from the prewarmed prompt file when the tool intent is weak). It emits updated_input only when at least one of those changes applies (resolved id, valid input.model, or intent replacement); otherwise it returns { permission: "allow" } with no updated_input. See integrations/cursor/hooks/AIC-inject-conversation-id.cjs (shouldEmitUpdatedInput).
{ permission: "allow", updated_input: { ...toolInput, editorId: "cursor", /* optional: conversationId, modelId, intent */ } }
// or, when nothing to change:
{ permission: "allow" }Without this hook (or when no real or synthetic id can be resolved), compilation_log rows from this Cursor session often have conversation_id = null, breaking aic_chat_summary for this conversation.
File: .cursor/hooks/AIC-inject-conversation-id.cjs
Event: postToolUse with matcher: "MCP"
Input fields used:
input.tool_name→ to identifyaic_compilecallsinput.tool_input→ confirmsintent+projectRootfields presentinput.tool_output→ checks for a non-emptycontentarray (success)
Purpose: After a successful aic_compile call, inject a short confirmation into the model's context:
"AIC compilation completed. Use the compiled context for your next response."
This gives the model a clear signal that the compilation result is available and should be used. Without this confirmation, the model may not reliably apply the compiled context.
Output: { additional_context: "..." } on success, {} on skip.
File: .cursor/hooks/AIC-post-compile-context.cjs
Event: beforeShellExecution with matcher: "git"
Input fields used:
input.command→ the full shell command string
Purpose: Block any git command that includes --no-verify or -n (the short form that skips hooks). Project rules (Husky + lint-staged) enforce formatting and linting in pre-commit hooks. An agent will sometimes try --no-verify; this hook prevents it deterministically.
Logic:
- Strip quoted strings from the command (so
--no-verifyinside a commit message-m "..."is not a false positive). - Check if the command contains
\bgit\band (--no-verifyor-n). - If both: deny with a clear agent message explaining why.
Output: { permission: "deny", user_message, agent_message } when blocked.
File: .cursor/hooks/AIC-block-no-verify.cjs
Event: afterFileEdit
Input fields used (flexible extraction):
input.files/input.paths/input.editedFiles/input.edited_paths→ array of pathsinput.file/input.path/input.filePath→ single pathinput.edit/input.edits→ nested object with path fieldinput.conversation_id/input.conversationId/input.session_id/AIC_CONVERSATION_ID→ temp file key
Purpose: Maintain a cumulative list of file paths the agent edited during the current conversation. Written under os.tmpdir() using integrations/shared/edited-files-cache.cjs as aic-edited-cursor-<conversation_key>.json (sanitized key). The stop hook reads this list to run lint and typecheck on only the touched files.
Why flexible extraction: Cursor's afterFileEdit input schema is not fully stable (v2 vs. earlier field names). The hook tries multiple field names to accommodate schema changes.
Output: {} always — side-effect only.
File: .cursor/hooks/AIC-after-file-edit-tracker.cjs
Event: stop
Input fields used:
input.conversation_id/input.session_id/AIC_CONVERSATION_ID→ temp file key (for edited files list)
Purpose: Before Cursor's agent reports "done", run ESLint and tsc --noEmit on every file edited this session (from §7.7's temp file). If either fails, return a followup_message so Cursor auto-submits a fix request:
"Fix lint and typecheck errors on the files you edited. Run pnpm lint and pnpm typecheck."
The model then sees this as a new prompt, fixes the errors, and tries to stop again. The
loop_limit: 5 in hooks.json caps retries.
Guard conditions:
- If the temp file doesn't exist → allow stop (no files edited, or tracker missed)
- If the file list is empty after filtering → allow stop
- If neither ESLint nor
tsc --noEmitare available → allow stop (notsconfig.json)
File: .cursor/hooks/AIC-stop-quality-check.cjs
Event: sessionEnd
Input fields used:
input.session_id→ logged to.aic/session-log.jsonlinput.reason→ reason Cursor ended the sessioninput.duration_ms→ session duration
Purpose:
- Cleanup: Delete
aic-gate-*,aic-deny-*, andaic-prompt-*temp files fromos.tmpdir()(per-generation state, prewarm prompts). The preToolUse gate (§7.3) also sweeps stale files opportunistically, throttled to once per 10 minutes. - Session log: Append one JSON line to
.aic/session-log.jsonlwithsession_id,reason,duration_ms,timestamp. Age-based pruning uses the same MCP startup path andshared/src/maintenance/prune-jsonl-by-timestamp.tshelper as the other.aic/*.jsonllogs.
Must never block: Exit 0 always. No stdout. If appendSessionLog fails, silently ignore.
File: .cursor/hooks/AIC-session-end.cjs
Event: subagentStart
Input fields used:
input.task→ truncated to 200 chars asintentforaic_compile(or"provide context for subagent"when missing)input.parent_conversation_id→ passed asconversationIdwhen present; otherwiseresolveConversationIdFallbackon the payload supplies a synthetic parent id forcompilation_logattributioninput.subagent_model→ when valid (trimmed length 1–256, printable ASCII), passed asmodelIdon theaic_compileJSON-RPCargumentsforcompilation_log.model_id; also appended to.aic/session-models.jsonlviawriteSessionModelCache(same as other hooks that record model id)- Cache fallback: when
input.subagent_modelis missing or invalid, the hook usesreadSessionModelCacheon.aic/session-models.jsonlfor this conversation and editorcursor, and uses that value asmodelIdif valid (same trimmed-length and printable-ASCII checks). This ensurescompilation_log.model_idis populated even when Cursor omitssubagent_modelfrom the payload. The read path matches the MCP compile handler: bounded tail of the JSONL file with deterministic full-file fallback (Implementation specification — Model id resolution; AIC JSONL caches).
Purpose: When a subagent (Task tool) is about to start, the hook calls aic_compile with triggerSource: "subagent_start" and the parent conversation ID. Cursor's subagentStart output schema does not support additional_context, so the hook does not inject context; it always returns permission: "allow". The sole purpose is so compilation_log has one row per subagent start with valid token data for telemetry.
Must never block: On parse or exec error the hook still returns permission: "allow" so subagent start is never blocked; the compile is best-effort.
File: .cursor/hooks/AIC-subagent-compile.cjs
Event: subagentStop
Reference: Cursor agent hooks — fires when a Task-tool subagent completes; paired with subagentStart for subagent lifecycle.
Input fields used:
input.conversation_id→ parent chat id when present; passed asconversationIdonaic_compileinput.parent_conversation_id→ alternate field for the parent id whenconversation_idis absent (first match wins:conversation_id, thenparent_conversation_id); when both are absent,resolveConversationIdFallbacksupplies a synthetic parent id when valid candidates existinput.agent_transcript_path→ path to the subagent transcript.jsonl;conversationIdFromAgentTranscriptPathinintegrations/shared/conversation-id.cjsyields the child session id (basename without.jsonl)
Purpose: Subagents run under a separate session id. Compilations inside the subagent would otherwise stay on that child id. When parent and child ids differ, the hook calls aic_compile with triggerSource: "subagent_stop", conversationId (parent), and reparentFromConversationId (child). The MCP compile handler runs reparentSubagentCompilations in shared/src/storage/reparent-subagent-compilations.ts only — no full compile pipeline — so existing compilation_log rows move to the parent. That keeps aic_chat_summary and per-conversation diagnostics on one thread for the whole chat.
Must never block: On parse or exec error the hook still writes {} to stdout; reparent is best-effort.
File: .cursor/hooks/AIC-subagent-stop.cjs
Cursor: Ten documented event types get AIC registrations; twelve hook command entries total (some types run two commands); thirteen script files deployed per aic-hook-scripts.json. Unused events are listed after the table.
| Event | AIC use | Notes |
|---|---|---|
sessionStart |
§7.1 | Two hooks: invariants (AIC-session-init) + compile (AIC-compile-context) |
beforeSubmitPrompt |
§7.2 | Prewarm gate, prompt log. No context injection (schema limitation) |
preToolUse (unmatched) |
§7.3 | aic_compile enforcement gate (emergency bypass when both devMode and skipCompileGate are true in aic.config.json) |
preToolUse (MCP) |
§7.4 | conversationId injection |
postToolUse (MCP) |
§7.5 | Compile confirmation |
beforeShellExecution (git) |
§7.6 | --no-verify blocker |
afterFileEdit |
§7.7 | File edit tracker |
stop |
§7.8 | Quality gate |
sessionEnd |
§7.9 | Cleanup + telemetry |
subagentStart |
§7.10 | Telemetry compile per subagent start |
subagentStop |
§7.11 | Reparent compilation_log from subagent session to parent conversation |
Not registered: preCompact and any other Cursor event without a row above — no entry in
hooks.json.template. subagentStart cannot inject context; it exists for compilation_log
telemetry only. subagentStop does not inject context; it exists for reparent only.
Cursor: Under .cursor/hooks/, AIC-compile-context.cjs, AIC-subagent-compile.cjs, and AIC-subagent-stop.cjs each spawn the MCP server over stdio JSON-RPC. The first two issue full aic_compile requests (session-wide context on sessionStart, subagent telemetry on subagentStart). The third issues a reparent-only aic_compile request when parent and child conversation ids are both known (triggerSource: "subagent_stop" — see §9.3).
Gate and session shape: Other compile paths reach the handler only after preToolUse when §7.3 is enforcing (default unless the emergency bypass is active — both devMode and skipCompileGate true in aic.config.json). Because beforeSubmitPrompt cannot emit additional_context, session-wide snapshot injection uses AIC-compile-context.cjs on sessionStart.
Resolve conversationId with conversationIdFromTranscriptPath then resolveConversationIdFallback (transcript/direct conversation fields first). The JSON-RPC call should include conversationId when that resolution yields a non-empty string:
// correct
const compileArgs = {
intent: INTENT,
projectRoot: projectRoot,
editorId: "cursor",
};
if (
conversationId &&
typeof conversationId === "string" &&
conversationId.trim().length > 0
) {
compileArgs.conversationId = conversationId.trim();
}Omitting conversationId when resolution fails leaves compilation_log.conversation_id null and breaks aic_chat_summary rollups for that compile.
Cold start: execFileSync("npx", …) spawns the MCP server (~500–1500ms TS compile on cold cache; 2–5s warm round-trip, up to ~10s first run; 20s hook timeout). The published package drops TS compile overhead (~200–500ms cold).
Same spawn pattern for a best-effort compilation_log row; on failure the hook still allows subagent start. Unlike §9.1, this hook does not use conversationIdFromTranscriptPath (no session transcript path on subagent start). It uses trimmed parent_conversation_id when present; otherwise resolveConversationIdFallback on the hook payload so telemetry can roll up when synthetic ids qualify (Integrations shared modules).
Same execFileSync("npx", …) + JSON-RPC spawn pattern as §9.2. The tool response is { reparented: true, rowsUpdated: N } JSON text (not compiledPrompt). On failure the hook still prints {} so the parent session continues. All other hooks are pure Node (under ~50ms), except the three in §9 opening paragraph.
Cursor: .cursor/hooks.json per project (merged by install.cjs when it runs):
{
"version": 1,
"hooks": {
"sessionStart": [
{ "command": "node .cursor/hooks/AIC-session-init.cjs" },
{ "command": "node .cursor/hooks/AIC-compile-context.cjs", "timeout": 20 }
],
"beforeSubmitPrompt": [
{ "command": "node .cursor/hooks/AIC-before-submit-prewarm.cjs" }
],
"preToolUse": [
{
"command": "node .cursor/hooks/AIC-require-aic-compile.cjs",
"failClosed": true
},
{ "command": "node .cursor/hooks/AIC-inject-conversation-id.cjs", "matcher": "MCP" }
],
"postToolUse": [
{ "command": "node .cursor/hooks/AIC-post-compile-context.cjs", "matcher": "MCP" }
],
"beforeShellExecution": [
{ "command": "node .cursor/hooks/AIC-block-no-verify.cjs", "matcher": "git" }
],
"afterFileEdit": [
{ "command": "node .cursor/hooks/AIC-after-file-edit-tracker.cjs" }
],
"sessionEnd": [{ "command": "node .cursor/hooks/AIC-session-end.cjs" }],
"subagentStart": [{ "command": "node .cursor/hooks/AIC-subagent-compile.cjs" }],
"subagentStop": [{ "command": "node .cursor/hooks/AIC-subagent-stop.cjs" }],
"stop": [
{ "command": "node .cursor/hooks/AIC-stop-quality-check.cjs", "loop_limit": 5 }
]
}
}When the MCP server runs integrations/cursor/install.cjs from a workspace root that contains that script, hooks are written or merged idempotently. Existing user entries outside the AIC-* namespace are preserved; stale AIC-* hook entries are pruned.
Cursor does not currently expose an HTTP hook type. When it does, the round-trip can be eliminated by pointing hooks to the already-running AIC MCP server's HTTP endpoint:
{
"type": "http",
"url": "http://localhost:${AIC_HTTP_PORT}/hooks/session-start",
"timeout": 5
}Benefits over the current per-invocation execFileSync("npx", …) child process approach:
- Zero cold start — MCP server already has SQLite connection, pipeline initialized, tiktoken loaded
- No
npx tsxoverhead — pre-compiled JS already running - Single HTTP round-trip vs. four process spawns (node + tsx + server + tool call)
This is a future optimization once Cursor exposes the HTTP hook type.
Cursor: No plugin system yet; distribution is installer-only.
Cursor: When the bootstrap gate passes (see §2) and Cursor is detected, runEditorBootstrapIfNeeded resolves the installer path: <project>/integrations/cursor/install.cjs if that file exists, otherwise the copy bundled in @jatbas/aic at integrations/cursor/install.cjs relative to the installed package, then runs node on that path with cwd at the project root. Same triggers as §3: roots listed (if supported) or first aic_compile per project.
listRoots (if supported) or first aic_compile
↓
runEditorBootstrapIfNeeded → resolve installer (in-project overrides bundled)
↓
node <resolved>/integrations/cursor/install.cjs
↓
.cursor/hooks/ + hooks.json merged
If bootstrap does not run (wrong editor detection) or you need a one-off refresh, run node on an installer path manually — installation.md.
The installer (integrations/cursor/install.cjs):
- Ensures
.cursor/hooks/directory exists - For each hook script in
AIC_SCRIPT_NAMES: reads current content fromintegrations/cursor/hooks/and writes to.cursor/hooks/only if content differs (idempotent — no double-writes) - Deletes any
AIC-*.cjsfiles in.cursor/hooks/that are not inAIC_SCRIPT_NAMES(stale script cleanup) - Reads
.cursor/hooks.json(if present) and merges AIC entries into existing user config, preserving non-AIC hooks - Installs the trigger rule (
.cursor/rules/AIC.mdc)
The installer is a standalone .cjs script with no dependency on mcp/src/. Source scripts live in integrations/cursor/hooks/. The installer copies them to .cursor/hooks/.
Cursor: .cursor/rules/AIC.mdc with alwaysApply: true instructs the model to call aic_compile first on every message. The preToolUse gate (§7.3) enforces that unless the emergency bypass is active (both devMode and skipCompileGate true in aic.config.json).
- Without the rule: the model may not call
aic_compileat all until blocked by the gate. The deny message then provides the intent, creating a round-trip penalty. - Without the gate (or when the emergency bypass is active): the rule is advisory only — the model can ignore it without consequence.
The trigger rule + gate are the primary delivery mechanism for per-intent context in Cursor when enforcement is active. Because there is no per-prompt context injection hook, these two components are essential for most workflows — not a fallback.
Auto-installed when the Cursor installer runs. Version-stamped so it is overwritten when the AIC package version changes.
| Bug | Issue | Status | Workaround |
|---|---|---|---|
afterFileEdit input schema varies across Cursor versions |
— | No issue filed | Flexible field extraction in AIC-after-file-edit-tracker.cjs (§7.7) |
conversation_id not passed to all hooks — only beforeSubmitPrompt and preToolUse receive it |
— | Cursor behavior — no issue filed | AIC_CONVERSATION_ID env var injected by sessionStart; used as fallback in other hooks |
subagentStart does not support additional_context output |
— | Cursor capability gap | No workaround — subagent context injection is structurally impossible in Cursor |
subagentStop does not support additional_context output |
— | Cursor capability gap | AIC uses it for reparent only (compilation_log); see §7.11 |
preCompact is observational only — no output injected into new context |
— | Cursor capability gap | No workaround — recompile after compaction is structurally impossible in Cursor |
All of the following must be verified for the Cursor integration to be complete:
Context delivery:
-
AIC-session-init.cjsinjects architectural invariants viaadditional_context(§7.1) -
AIC-compile-context.cjscallsaic_compilewithconversationIdresolved per §9.1 (transcript path, direct ids,AIC_CONVERSATION_ID, orresolveConversationIdFallback) -
AIC-before-submit-prewarm.cjssaves prompt for gate deny message (§7.2) -
AIC-require-aic-compile.cjsenforcesaic_compilevia per-generation marker, default 300s recency fallback (compileRecencyWindowSecsoverride), sibling-race poll before deny, and indefinite deny until compile or markers unless emergency bypass is active (§7.3) -
AIC-inject-conversation-id.cjsinjectsconversationIdinto MCP args (§7.4) -
AIC-post-compile-context.cjsinjects confirmation after compile (§7.5) -
AIC-subagent-compile.cjsruns telemetryaic_compileon subagentStart (§7.10) -
AIC-subagent-stop.cjsruns reparentaic_compileon subagentStop (§7.11)
Quality gate (Cursor-specific):
-
AIC-after-file-edit-tracker.cjsrecords edited files to temp file (§7.7) -
AIC-stop-quality-check.cjsruns lint/typecheck, usesfollowup_message(§7.8) -
AIC-block-no-verify.cjsblocks--no-verifyviabeforeShellExecution(§7.6)
Settings:
-
hooks.jsonmatches template: 12 command entries across ten event keys, includingsubagentStartandsubagentStop(§10)
Init behavior:
- Bootstrap:
install.cjsruns from in-project path when present, else bundled package copy (§2–§3); when only the packaged layout is available, run the installer manually per installation.md#first-compile-bootstrap)
Temp file conventions:
-
aic-gate-<generation_id>: written by preToolUse gate on successfulaic_compile, deleted by sessionEnd and opportunistic cleanup (not written when emergency bypass is active — §7.3) -
aic-gate-recent-<project_hash>: project-scoped recency marker written on successfulaic_compile, excluded from opportunistic cleanup (§7.3) -
aic-gate-cleanup-marker: throttle stamp for opportunistic cleanup; sweep runs at most once per 10 minutes (§7.3) -
aic-prompt-<generation_id>: written by beforeSubmitPrompt, deleted by sessionEnd and opportunistic cleanup -
aic-edited-cursor-<conversation_key>.jsonunderos.tmpdir(): written by afterFileEdit, read by stop (not removed by sessionEnd; overwritten per session key)
integrations/cursor/uninstall.cjs defaults to project-local cleanup: <project>/.cursor/mcp.json, project hooks and AIC.mdc, and (unless --keep-project-artifacts) project aic.config.json, .aic/, matching ignore-file lines, and the AIC managed span in .claude/CLAUDE.md. --global adds ~/.cursor/mcp.json, global Claude Code AIC state under ~/.claude/ (because Cursor bootstrap can install Claude hooks), and ~/.aic/ cleanup (SQLite preserved unless --global --remove-database or env overrides). devMode: true in aic.config.json skips all changes unless --force. Full ordering, flags, and bundled paths under node_modules/@jatbas/aic are documented in installation.md § Uninstall.