This document is the single source of truth for building and maintaining the Claude Code integration layer. It covers the hook scripts in integrations/claude/hooks/ (source) and their deployment to ~/.claude/hooks/ (editor target), the settings wiring in ~/.claude/settings.json, the adapter pattern, and every known bug with its workaround.
The AIC core (anything in shared/ or mcp/src/) has zero knowledge of Claude Code.
All Claude Code-specific source code lives in integrations/claude/. The .claude/ directory and ~/.claude/ are deployment targets that Claude Code reads — they are not source directories. This distinction is not a preference — it is a structural invariant.
What this means concretely:
-
All Claude Code hook and settings logic lives in
integrations/claude/. The installer (integrations/claude/install.cjs) is standalone.mcp/src/editor-integration-dispatch.tsdetects a Claude Code workspace when.claude/exists,CLAUDE_PROJECT_DIRis set, or (in auto mode with Cursor detected) the Anthropic Claude Code extension is present under~/.cursor/extensions(directory name prefixanthropic.claude-code). It resolves the installer path the same way as Cursor:<project>/integrations/claude/install.cjswhen that file exists under the workspace root, otherwise the copy bundled inside the published@jatbas/aicpackage at package-relativeintegrations/claude/install.cjs, then runs it withexecFileSyncwhen the MCP client lists workspace roots (if the client supports roots) or on the firstaic_compilefor that project (mcp/src/server.ts/mcp/src/handlers/compile-handler.ts). It does not duplicate copy/merge logic. Manual run:node integrations/claude/install.cjs. Idempotent (writes only when content differs). -
The
aic_compileMCP tool is neutral. It requiresintentandprojectRoot; shipped Claude Code hooks also sendeditorId,triggerSource, and oftenconversationId/modelId. It does not know who called it. The hook adapter inintegrations/claude/hooks/translates Claude Code'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 that call aic_compile through aic-compile-helper.cjs do not set toolOutputs or relatedFiles (that helper sends intent, projectRoot, editorId, and optional conversationId, triggerSource, modelId only). 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 Claude Code, it goes inintegrations/claude/.
Claude Code: Hook settings are read from the same files by all three deployment modes:
| Mode | Hook support | Settings source |
|---|---|---|
CLI (claude) |
Full Claude Code hook lifecycle (all hook events the product exposes) | ~/.claude/settings.json + .claude/settings.json |
VS Code extension (anthropic.claude-code) |
Same hooks via shared settings | Same files — settings shared between CLI and extension (IDE integrations docs) |
Cursor extension (cursor:extension/anthropic.claude-code) |
Same | Same files — supported install target (IDE integrations docs) |
One set of hook scripts, one settings file. All three modes pick them up without any mode-specific code. The VS Code docs explicitly confirm: "Claude Code settings in ~/.claude/settings.json: shared between the extension and CLI. Use for allowed commands, environment variables, hooks, and MCP servers." (source)
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 Claude Code's hook protocol into an
aic_compile MCP call:
Claude Code runtime
│
│ stdin: { prompt, cwd, session_id, hook_event_name, … }
▼
~/.claude/hooks/aic-compile-helper.cjs ← shared MCP caller (used by several hooks)
│
│ JSON-RPC: tools/call aic_compile (intent, projectRoot, editorId, optional session fields — §2.1)
▼
mcp/src/server.ts → CompilationRunner.run()
│
│ result: { compiledPrompt, … }
▼
~/.claude/hooks/<hook>.cjs (deployed script finishes the event)
│
│ stdout: plain text or hookSpecificOutput JSON (see §6)
▼
Claude Code runtime → injects as context
Two transports to the same server
The diagram above shows the hook-driven path end-to-end. The same
mcp/src/server.tspipeline andaic_compiletool also appear when Claude Code's registered MCP client invokes the tool; caller, framing, and host limits differ:
- Hook-driven path (context hooks): Hooks invoke
aic-compile-helper.cjs, which spawns a short-lived server process and exchanges MCP-style JSON-RPC over that process's stdio (initialize,notifications/initialized,tools/callforaic_compile— seeintegrations/claude/hooks/aic-compile-helper.cjs). The registered MCP client is not on this path; only the hook-spawned child performs JSON-RPC to the server.- Registered MCP tool path: When tool execution uses the
aicMCP server from~/.claude/settings.json(see §10),aic_compileruns through Claude Code's MCP client with normal tool-result handling. Host-level result-size limits apply. From Claude Code 2.1.91, large tool results may declare_meta["anthropic/maxResultSizeChars"](up to 500,000 characters) on the tool result; that applies to registered MCP tool results, not to the hook-driven pipe in item 1. See the Claude Code changelog (v2.1.91 notes) and anthropics/claude-code#42869 for_metacontext where official MCP docs lag.
The adapter (aic-compile-helper.cjs) and all event hooks are authored in
integrations/claude/hooks/. The installer writes them only under ~/.claude/hooks/.
When run from a project directory that is not the user home, it also removes legacy
aic-*.cjs copies from that project’s .claude/hooks/. Nothing outside
integrations/claude/ changes at dev time.
| Claude Code hook input field | AIC aic_compile argument |
|---|---|
input.prompt (UserPromptSubmit) |
intent |
input.agent_type (SubagentStart) |
part of intent string |
| Generic for session events | fixed intent string "understand project structure, architecture, and recent changes" |
input.cwd |
projectRoot (fallback to $CLAUDE_PROJECT_DIR → process.cwd()) |
input.transcript_path |
Primary source for conversationId via conversationIdFromTranscriptPath — basename without .jsonl |
input.conversation_id (top-level or nested under input) |
Fallback for conversationId when no non-empty transcript path — trimmed string (same helper; see Integrations shared modules) |
explicitEditorId (derived, not a wire field) |
explicitEditorIdFromClaudeHookEnvelope(parsed) → cursor-claude-code when only direct conversation_id is present without transcript path; otherwise claude-code. Passed through callAicCompile for correct compilation_log.editor_id. |
input.model (SessionStart when present) |
modelId; SessionStart passes it; other hooks use readSessionModelCache on .aic/session-models.jsonl (written when SessionStart or other hooks record a valid model) |
readSessionModelCache uses the same bounded tail read and deterministic full-file fallback as the MCP compile handler (Implementation specification — Model id resolution; AIC JSONL caches).
Resolving conversationId: Hooks use conversationIdFromTranscriptPath(parsed) in integrations/shared/conversation-id.cjs first (transcript basename, then trimmed direct conversation_id). When that returns null, compile/inject/reparent hooks call resolveConversationIdFallback(parsed) so aic_compile still receives a deterministic synthetic id when parent_conversation_id, session_id, or generation_id / camelCase variants yield a valid candidate (printable ASCII, max 128 chars — see Integrations shared modules). Claude Code normally includes transcript_path in hook input (common input fields); the UUID in the transcript filename is stable across hooks in the same chat. aic-compile-helper forwards conversationId and explicit editorId from the caller.
Entry guard: If cursor_version or input.cursor_version is present, Claude hooks return immediately without calling aic_compile — see Runtime boundary guards (cursor_version).
Note: Synthetic fallbacks may use
session_idorgeneration_idwhen transcript/direct ids are absent — they stabilizecompilation_logattribution for those hook paths but are not the transcript UUID. Prefer transcript/direct ids when the host supplies them.
integrations/claude/ is the source. Deployment target for hook registration is
~/.claude/ (global) only. Nothing in mcp/ or shared/ changes. The installer deploys every listed script to ~/.claude/hooks/; aic-compile-helper.cjs is deployed and is required at runtime by the context hooks (aic-prompt-compile, aic-session-start, aic-subagent-inject, aic-pre-compact).
integrations/claude/ ← SOURCE (authored here)
hooks/
aic-compile-helper.cjs # Protocol adapter (deployed; required by context hooks)
aic-prompt-compile.cjs # UserPromptSubmit — PRIMARY context delivery
aic-session-start.cjs # SessionStart — bootstrapping + post-compaction
aic-subagent-inject.cjs # SubagentStart — subagent context injection
aic-subagent-stop.cjs # SubagentStop — reparent compilation_log rows
aic-after-file-edit-tracker.cjs # PostToolUse(Edit|Write) — feeds Stop hook
aic-stop-quality-check.cjs # Stop — ESLint + typecheck quality gate
aic-block-no-verify.cjs # PreToolUse(Bash) — block --no-verify git commits
aic-pre-tool-gate.cjs # PreToolUse(unmatched) — blocks non–aic_compile tools until recency / turn markers pass
aic-inject-conversation-id.cjs # conversationId injection for MCP calls (registered in settings template)
aic-pre-compact.cjs # PreCompact — re-inject context before compaction
aic-session-end.cjs # SessionEnd — telemetry only
install.cjs # Installer: deploys hooks + merges settings
~/.claude/hooks/ ← DEPLOYMENT TARGET (global, created by install.cjs)
aic-*.cjs # Deployed from integrations/claude/hooks/
~/.claude/settings.json # AIC hook entries merged in by install.cjs
.claude/ (in project, optional) ← CLAUDE.md — Fallback trigger rule only
CLAUDE.md # Written by install.cjs when run from project dir
Claude Code's hooks use two distinct output mechanisms: plain text stdout for UserPromptSubmit, and JSON hookSpecificOutput for the events listed in the subsections below. Getting this wrong produces silent drops or "hook error" banners.
Recommended: write plain text directly to stdout. Any non-JSON text is added as context (docs):
// RECOMMENDED — works on every message including first
process.stdout.write(compiled);The docs also support a JSON format with hookSpecificOutput, but issue
#17550 (filed Jan 2026, closed as not_planned) shows that hookSpecificOutput JSON causes a "UserPromptSubmit hook error" on the first message of every new session. The hook runs correctly (exit code 0, valid JSON), but Claude Code's session-init path cannot process it. Anthropic declined to fix this. Plain text avoids the bug entirely and is the documented first-class path.
// AVOID — triggers #17550 on first message of new session
process.stdout.write(
JSON.stringify({
hookSpecificOutput: {
hookEventName: "UserPromptSubmit",
additionalContext: compiled,
},
}),
);Last verified (Claude Code build): [On first merge, leave this placeholder. After each checklist run, replace with the exact version line printed by claude --version on the host where you ran the repro, then add a short outcome note.]
Re-verification at each Claude Code major or minor release:
- Start a new session. Reproduce the #17550 failure mode by emitting
hookSpecificOutputJSON forUserPromptSubmit(same shape as the AVOID example above). Run the repro from a throwaway branch with experimental hook edits so production hooks stay on plain text. - If Claude Code no longer surfaces a "UserPromptSubmit hook error" on the first message, reassess
integrations/claude/hooks/aic-prompt-compile.cjsand whether the dual-path marker logic remains necessary. Keep JSONhookSpecificOutputoff the productionUserPromptSubmitpath until that reassessment completes. - If the error still appears, keep plain-text stdout. Update Last verified with the exact version string tested and a one-line outcome describing that the first-message error still reproduces.
- Implementation reference:
integrations/claude/hooks/aic-prompt-compile.cjsusesprocess.stdout.writefor hook output andisSessionAlreadyInjectedfromintegrations/shared/session-markers.cjsfor the SessionStart fallback (§7.2).
For SessionStart, the official docs only document the hookSpecificOutput format for additionalContext injection. There is no plain text path specified for this event. Use the documented format:
process.stdout.write(
JSON.stringify({
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: text,
},
}),
);Note: issue #10373 (open since Oct 2025) means this hook's output is silently discarded for brand new interactive sessions in CLI mode. The format is not the problem — the hook fires but its output is dropped at the session-init path. Workaround: dual-path injection via
UserPromptSubmit(see §7.2).
Same behavior as SessionStart. The docs only document hookSpecificOutput for additionalContext injection on this event:
process.stdout.write(
JSON.stringify({
hookSpecificOutput: {
hookEventName: "SubagentStart",
additionalContext: text,
},
}),
);process.stdout.write(
JSON.stringify({
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "...",
},
}),
);process.stdout.write(
JSON.stringify({
decision: "block",
reason:
"Fix lint and typecheck errors on the files you edited. Run pnpm lint and pnpm typecheck.",
}),
);The sample shows the both-fail case. If only ESLint fails, reason is Fix lint errors on the files you edited. Run pnpm lint and pnpm typecheck.; if only typecheck fails, reason uses typecheck in place of lint in that pattern. The hook never appends eslint or tsc stderr to reason.
These hooks have no context-injection or decision output. Exit 0 with no stdout (or empty
{}) signals success.
All 18 Claude Code lifecycle events are documented at code.claude.com/docs/en/hooks#hook-lifecycle. AIC uses nine of them for integration work (see §8). Known product bugs and workarounds are called out per hook below.
Reference: hooks#userpromptsubmit
Why this is AIC's core value for Claude Code: Fires before every user message, before the model processes it. The user's actual prompt text is available as input.prompt — this is the intent. No trigger rule needed; no model compliance required. Context delivery becomes deterministic and automatic.
Input fields used:
input.prompt→intentforaic_compileinput.transcript_path(and related fields) →conversationIdforaic_compileviaconversation-id.cjs(see §4.3)input.cwd→projectRootfallback
Output: Plain text stdout (see §6.1).
Matcher: UserPromptSubmit does not support matchers — it fires on every prompt
(matcher reference). Do not add a
matcher field; it is silently ignored.
Known bug — hookSpecificOutput format: See §6.1. Fix: plain text stdout.
File: .claude/hooks/aic-prompt-compile.cjs
When this hook writes a gate prewarm fragment under os.tmpdir() (aic-prompt-cc-*), new files use POSIX mode 0o600 (owner-only), matching the Cursor beforeSubmitPrompt hygiene in §7.2 there.
Reference: hooks#sessionstart
Purpose: Inject architectural invariants and a broad project context snapshot at the start of a session. model from hook input is passed as modelId to aic_compile; other hooks that call the helper resolve model id via readSessionModelCache on .aic/session-models.jsonl when SessionStart (or another hook) has recorded a valid model (same JSONL read semantics as the MCP handler — Implementation specification). Also fires on compact — re-injecting context after compaction is the primary reliable use case.
Matcher values: startup, resume, clear, compact
(matcher reference).
The hook as written uses no matcher (fires on all four).
Known bug — not firing for new sessions in CLI: Issue
#10373 (filed Oct 2025, still open as of Mar 2026). SessionStart fires on /clear, /compact, and URL resume, but not on brand new interactive sessions in CLI mode. The hook executes but its output is discarded. This is a runtime processing bug, not a format issue.
Workaround — dual-path injection: Because SessionStart is unreliable for new sessions,
UserPromptSubmit acts as a fallback for the first prompt. Pattern:
// aic-session-start.cjs writes a marker file on success:
const INJECTED_MARKER = path.join(projectRoot, ".aic", ".session-context-injected");
fs.writeFileSync(INJECTED_MARKER, sessionId);
// aic-prompt-compile.cjs reads the marker:
const alreadyInjected =
fs.existsSync(INJECTED_MARKER) &&
fs.readFileSync(INJECTED_MARKER, "utf8").trim() === sessionId;
if (!alreadyInjected) {
// SessionStart missed this session — inject architectural context now
const sessionContext = buildSessionContext(projectRoot);
parts.unshift(sessionContext);
}
// Always inject prompt-specific context
parts.push(promptContext);The marker is scoped by session_id so multiple concurrent sessions don't interfere. Delete the marker in SessionEnd.
Lock file layout (.session-start-lock), merge options, and ordering with this marker are recorded in Session start lock and session context marker.
Output: hookSpecificOutput JSON (see §6.2).
File: .claude/hooks/aic-session-start.cjs
Reference: hooks#subagentstart
Why this matters: Subagents lose parent context. Without this hook, every Claude Code subagent (Bash, Explore, Plan, or custom agents from .claude/agents/) starts without knowing the project architecture or conventions.
Input fields used:
input.agent_type→"Bash","Explore","Plan", or custom agent nameinput.transcript_path→conversationId(viapath.basename)input.prompt→intent(with IDE markup blocks like<ide_selection>…</ide_selection>and other<ide_*>regions stripped; falls back to agent_type-based intent when absent)
Matcher: Use "*" or omit — inject context into all subagent types. Use an Explore|Plan matcher when Bash subagents should not receive full context.
Output: hookSpecificOutput JSON (see §6.3).
File: .claude/hooks/aic-subagent-inject.cjs
Reference: hooks#pretooluse
Bash matcher — --no-verify blocker
Purpose: Block any git command that includes --no-verify or -n. Project rules forbid skipping pre-commit hooks (Husky + lint-staged enforce formatting and linting). An agent sometimes adds --no-verify to get past a failing commit — this hook stops it deterministically, not via instruction.
Matcher: Bash — fires only on Bash tool calls.
Decision output: hookSpecificOutput with permissionDecision: "deny" (see §6.4).
Input fields used: input.tool_input.command
File: .claude/hooks/aic-block-no-verify.cjs
MCP matcher — conversationId injection
Purpose: Ensure aic_compile receives conversationId, editorId, and often modelId in tool arguments when the model omits them. settings.json.template registers aic-inject-conversation-id.cjs only for matcher mcp__.*__aic_compile (there is no separate PreToolUse matcher for aic_chat_summary; on Claude Code, chat-summary attribution relies on earlier hooks passing conversationId from transcript and fallback resolution — Cursor injects aic_chat_summary separately; see Cursor integration layer §7.4). Other MCP tools such as aic_model_test are not covered by this matcher.
Behaviour (implementation): Parse stdin JSON. If isCursorNativeHookPayload (integrations/cursor/is-cursor-native-hook-payload.cjs) is true — cursor_version / input.cursor_version, or fresh editor-runtime-marker for cursor when conversationId resolves — return permissionDecision: "allow" with no updatedInput so Cursor-shaped invocations do not mutate MCP args on this hook path. Otherwise resolve conversationId as conversationIdFromTranscriptPath(parsed) ?? resolveConversationIdFallback(parsed) (Integrations shared modules). Treat the tool call as aic_compile when the tool name matches aic_compile or the payload looks like a compile request (intent and projectRoot strings). If not aic_compile, allow with no mutation. Set editorId to cursor-claude-code when CURSOR_TRACE_ID is set, else claude-code. Merge conversationId, editorId, and modelId from readSessionModelCache when the cached model normalises to something other than auto. When intent is weak (isWeakAicCompileIntent) and conversationId is set, replace intent from readAicPrewarmPrompt(\cc-${conversationId}`)(temp file underos.tmpdir()`), matching the prewarm key used on this path.
File: .claude/hooks/aic-inject-conversation-id.cjs (see §10 for registration)
Reference: hooks#pretooluse — unmatched PreToolUse runs before other PreToolUse matchers in integrations/claude/settings.json.template.
Purpose: Block tool calls that are not an aic_compile path until the current conversation has a fresh compile marker or passes the shared recency window (same intent as Cursor AIC-require-aic-compile.cjs, different stdin shape).
Early exits: Returns {} when isCursorNativeHookPayload is true (Cursor-shaped payloads must not run Claude enforcement). Resolves projectRoot by walking parents until aic.config.json exists (findEffectiveProjectRoot in the script).
Compile detection (isAicCompileCall): Allows when the tool name matches aic_compile (case-insensitive), when ToolSearch runs with a query matching aic_compile (schema discovery escape hatch), or when tool_input carries both intent and projectRoot strings.
Allow path: On a compile hit, calls writeCompileRecency(projectRoot) and returns allow (empty hook output).
Otherwise: Allows when isTurnCompiled(projectRoot, conversationId) or isCompileRecent(projectRoot) from integrations/shared/compile-recency.cjs succeeds (default recency 300 seconds unless compileRecencyWindowSecs in aic.config.json overrides). If still blocked, polls at 20 ms intervals for up to 500 ms (SIBLING_POLL_INTERVAL_MS / SIBLING_POLL_TOTAL_MS in aic-pre-tool-gate.cjs), re-checking those markers so a sibling aic_compile in the same parallel batch can satisfy the gate before a deny. If still blocked, returns a deny hookSpecificOutput with suggested intent from readAicPrewarmPrompt(\cc-${conversationId}`)`. There is no deny-count cap (same sibling-race story as Cursor; rationale in Cursor integration layer §7.3).
File: .claude/hooks/aic-pre-tool-gate.cjs
Reference: hooks#posttooluse
Purpose: Record every file path the agent edits during a session into a temp file keyed by
session_id. The Stop hook (§7.6) reads this list to run lint and typecheck only on touched files. Without the tracker, the Stop hook has no file list to operate on.
Matcher: Edit|Write — regex, matches both tool names
(matcher patterns).
Input fields used:
input.tool_input.path→ the absolute path of the file that was edited or writteninput.session_id→ temp file key
Output: Empty JSON {} — this hook has no decision to make, only a side effect (write to temp file). Exit 0.
File: .claude/hooks/aic-after-file-edit-tracker.cjs
Reference: hooks#stop
Purpose: Before Claude reports "done", read the touched-file list from the tracker in §7.5. Run ESLint with those .ts / .js paths and run tsc --noEmit once at the project root (TypeScript does not receive a per-file path list).
If either command exits non-zero, the hook returns JSON with decision: "block" and the generic reason pattern in §6.5 (failed stages only, fixed pnpm lines, no subprocess stderr in the string). Claude Code surfaces that payload as the stop denial so the agent can fix issues and try stopping again.
Matcher: Stop does not support matchers — fires on every stop.
Input fields used: input.session_id → temp file key (to load the edited-files list)
Implementation note: If the temp file for this session_id does not exist (no files were edited, or the tracker missed it), exit 0 immediately — do not block.
Implementation note: If tsconfig.json is absent at the project root, the hook does not invoke tsc (treated as success); ESLint alone can still produce decision: "block".
File: .claude/hooks/aic-stop-quality-check.cjs
Reference: hooks#precompact
Purpose: Fires before Claude Code compacts the context window. AIC can inject a fresh compilation so the model retains the most relevant project context through the compaction boundary. This is an observational/enrichment hook — it cannot prevent compaction.
Matcher values: manual, auto
Output: Plain text stdout (same as §6.1 — UserPromptSubmit rule applies here; plain text is the safe path).
File: .claude/hooks/aic-pre-compact.cjs
Reference: hooks#sessionend
Purpose: Log session lifecycle data to .aic/prompt-log.jsonl. The hook appends lines only.
Age-based pruning runs when the AIC MCP server starts (shared/src/maintenance/prune-jsonl-by-timestamp.ts).
No context injection — this hook produces no stdout. Exit 0 always (telemetry must never block the session from ending).
Input fields used: input.session_id, input.reason
Additional responsibility: Delete the .aic/.session-context-injected marker for this session_id (see the dual-path workaround in §7.2) so it doesn't persist across sessions. The hook also calls releaseSessionLock; see Session start lock and session context marker for lock and marker interaction.
File: .claude/hooks/aic-session-end.cjs
Reference: hooks#subagentstop
Purpose: When a subagent finishes, reparent that subagent’s compilation_log rows to the parent conversation so aic_chat_summary and diagnostics roll up correctly. Implemented by calling aic_compile with triggerSource: "subagent_stop", conversationId = parent id, and reparentFromConversationId = child id (handler reparent path in implementation-spec — Agentic workflows under Pipeline orchestration).
Input fields used: Parent resolution uses the same transcript and fallback chain as other hooks (conversationIdFromTranscriptPath then resolveConversationIdFallback). Child id comes from conversationIdFromAgentTranscriptPath on agent_transcript_path / input.agent_transcript_path. Cursor-native envelopes (cursor_version) exit without MCP I/O.
File: .claude/hooks/aic-subagent-stop.cjs (see §10). Cursor equivalent: cursor-integration-layer §7.11.
Claude Code: Of 18 lifecycle events (table), AIC registers nine and skips nine.
| Event | AIC use | Reason skipped |
|---|---|---|
SessionStart |
§7.2 | — |
UserPromptSubmit |
§7.1 | — |
SubagentStart |
§7.3 | — |
PreToolUse (Bash + MCP) |
§7.4 | — |
PostToolUse (Edit|Write) |
§7.5 | — |
Stop |
§7.6 | — |
PreCompact |
§7.7 | — |
SessionEnd |
§7.8 | — |
SubagentStop |
§7.9 | Reparents subagent compilation_log rows to the parent conversation via aic_compile (not the Stop quality gate). Cursor: same reparent role — cursor-integration-layer §7.11. |
PostToolUse (aic_compile) |
skipped | UserPromptSubmit already runs aic_compile before the model starts. Model-triggered aic_compile calls are a fallback only; confirming them adds noise without value. |
PostToolUseFailure |
skipped | No AIC-specific recovery action on tool failure. |
PermissionRequest |
skipped | Not AIC's concern — no policy to enforce here. |
Notification |
skipped | Observational only; no value for AIC. |
InstructionsLoaded |
skipped | Fires when CLAUDE.md loads; AIC has no audit requirement here. |
ConfigChange |
skipped | No AIC policy triggered by config changes. |
TeammateIdle |
skipped | Agent teams feature, out of scope. |
TaskCompleted |
skipped | Agent teams feature, out of scope. |
WorktreeCreate / WorktreeRemove |
skipped | Out of scope for the current AIC integration (lifecycle hooks only; not Git worktrees). |
Claude Code: aic-session-start.cjs and aic-prompt-compile.cjs call callAicCompile from aic-compile-helper.cjs for full aic_compile runs. aic-subagent-inject.cjs uses the same helper with triggerSource: "subagent_start" for compilation_log telemetry. aic-subagent-stop.cjs issues a narrow stdio tools/call aic_compile reparent sequence (reparent-only path; no full pipeline). aic-pre-compact.cjs also uses the helper. Compile gates and PreToolUse behaviour are in §7.
The shared helper mediates between a hook script and the AIC MCP server via MCP stdio. Its signature in integrations/claude/hooks/aic-compile-helper.cjs is:
callAicCompile(
intent,
projectRoot,
conversationId,
timeoutMs,
triggerSource,
modelId,
explicitEditorId,
);Emergency bypass: Before spawning the MCP server, the helper checks aic.config.json via isCompileGateSkipped() (from integrations/shared/read-project-dev-mode.cjs). When both devMode and skipCompileGate are true, it returns null immediately without any I/O. This prevents all compilation hooks (SessionStart, UserPromptSubmit, SubagentStart) from hanging on a broken MCP server. Remove skipCompileGate from the config immediately after resolving the issue.
Shipped hooks resolve conversationId before building MCP arguments from integrations/shared/conversation-id.cjs (Integrations shared modules). The example below matches that resolution order inside the JSON-RPC body real hooks build (outer jsonrpc / id omitted).
const {
conversationIdFromTranscriptPath,
resolveConversationIdFallback,
} = require("../../shared/conversation-id.cjs");
const conversationId =
conversationIdFromTranscriptPath(parsed) ?? resolveConversationIdFallback(parsed);
// ...
JSON.stringify({
method: "tools/call",
params: {
name: "aic_compile",
arguments: {
intent,
projectRoot,
editorId,
...(conversationId ? { conversationId } : {}),
// optional: triggerSource, modelId (hook args and helper-side model resolution)
},
},
});The snippet omits the outer jsonrpc / id envelope; the helper always supplies editorId via detectEditorId() in the same file.
Without a resolvable conversationId, compilation_log rows from Claude Code hooks have null conversation_id, and aic_chat_summary cannot aggregate them. Prefer transcript- or direct-conversation_id-derived ids. resolveConversationIdFallback may use validated session_id or generation_id only when they pass printable-ASCII length checks — they stabilise telemetry when the transcript UUID is unavailable; they are not interchangeable with the transcript filename id as the long-term conversation key when the host supplies the latter.
Cold start: Each hook invocation spawns a new process for the MCP stdio child. In integrations/claude/hooks/aic-compile-helper.cjs, when mcp/src/server.ts exists, the helper always launches it with spawn("npx", ["tsx", serverPath], { cwd: projectRoot }) where serverPath is the resolved absolute path to that file — argv-only, so projectRoot and serverPath are never parsed by a shell.
When shared/package.json is also present, the helper compares .ts mtimes under shared/src and mcp/src against a temp-directory stamp keyed by the project root; when sources are newer, it runs spawnSync("pnpm", ["--filter", "@jatbas/aic-core", "build"], { cwd: projectRoot, stdio: ["ignore", "inherit", "inherit"] }), returns null on non-zero status, otherwise appends an empty write to the stamp (fs.writeFileSync with { flag: "a" }) so later invocations skip the build until sources change again. When mcp/src/server.ts is absent, the helper runs spawn("npx", ["@jatbas/aic"], { cwd: projectRoot }) for the published package.
On a cold filesystem cache the dev-oriented path is often ~500–1500ms before the first response. §10 hook entries use 30s; the helper uses 25s only when timeoutMs is omitted — shipped compile hooks pass 30s, so runtime matches §10. For hook-spawned stdio versus Claude Code's registered MCP client, see §4.1 Two transports to the same server. §11 describes the HTTP hook path that avoids per-invocation spawn cost.
aic-session-start.cjs calls callAicCompile with a fixed session-wide intent, 30s timeout, triggerSource "session_start", optional modelId, and explicitEditorId from the hook envelope. aic-prompt-compile.cjs calls callAicCompile with the classified user intent, 30s, triggerSource "prompt_submit", optional modelId, and explicitEditorId. Both paths use the conversationId resolution described in §9.1. Registration and command paths appear in §10.
aic-subagent-inject.cjs calls callAicCompile with a derived subagent intent, 30s, triggerSource "subagent_start", and optional modelId. When callAicCompile returns null, the hook script still writes {} to stdout so the subagent can start (best-effort telemetry).
aic-subagent-stop.cjs does not use callAicCompile; it builds stdio JSON-RPC (initialize, notifications/initialized, tools/call on aic_compile with triggerSource: "subagent_stop", parent conversationId, and reparentFromConversationId for the child) via execFileSync("npx", …) in integrations/claude/hooks/aic-subagent-stop.cjs (reparent-only tool response, not a full compile).
Claude Code: The JSON below merges into the hooks section of ~/.claude/settings.json (global). The shipped settings.json.template uses $HOME in command paths instead of ~ for shell portability; structure matches.
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "node \"$HOME/.claude/hooks/aic-session-start.cjs\"",
"timeout": 30,
"statusMessage": "Compiling AIC project context..."
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "node \"$HOME/.claude/hooks/aic-prompt-compile.cjs\"",
"timeout": 30,
"statusMessage": "Compiling intent-specific context..."
}
]
}
],
"SubagentStart": [
{
"hooks": [
{
"type": "command",
"command": "node \"$HOME/.claude/hooks/aic-subagent-inject.cjs\"",
"timeout": 30,
"statusMessage": "Compiling subagent context..."
}
]
}
],
"SubagentStop": [
{
"hooks": [
{
"type": "command",
"command": "node \"$HOME/.claude/hooks/aic-subagent-stop.cjs\"",
"timeout": 10
}
]
}
],
"PreCompact": [
{
"hooks": [
{
"type": "command",
"command": "node \"$HOME/.claude/hooks/aic-pre-compact.cjs\"",
"timeout": 30,
"statusMessage": "Compiling pre-compaction context..."
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "node \"$HOME/.claude/hooks/aic-after-file-edit-tracker.cjs\""
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "node \"$HOME/.claude/hooks/aic-stop-quality-check.cjs\"",
"timeout": 60,
"statusMessage": "Running lint and typecheck..."
}
]
}
],
"PreToolUse": [
{
"hooks": [
{
"type": "command",
"command": "node \"$HOME/.claude/hooks/aic-pre-tool-gate.cjs\"",
"timeout": 10,
"statusMessage": "Checking AIC compile gate..."
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "node \"$HOME/.claude/hooks/aic-block-no-verify.cjs\""
}
]
},
{
"matcher": "mcp__.*__aic_compile",
"hooks": [
{
"type": "command",
"command": "node \"$HOME/.claude/hooks/aic-inject-conversation-id.cjs\""
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "node \"$HOME/.claude/hooks/aic-session-end.cjs\""
}
]
}
]
}
}The hooks API supports type: "http" alongside type: "command"
(HTTP hook fields). Claude Code sends the hook's JSON input as an HTTP POST request body. The response body uses the same output format as command hooks:
2xxwith plain text body → text added as context2xxwith JSON body → parsed using the same JSON output schema
Since the AIC MCP server is already running when Claude Code is active, the MCP server can expose a lightweight HTTP endpoint for the hook URL in settings.json. The hook configuration changes from spawning a Node process to a single HTTP round-trip:
{
"type": "http",
"url": "http://localhost:${AIC_HTTP_PORT}/hooks/user-prompt-submit",
"timeout": 15
}Benefits:
- Zero cold start — MCP server already has SQLite connection, pipeline initialized, tiktoken loaded
- No
npx tsxoverhead — pre-compiled JS already running - Response body is plain text (§6.1 rule applies)
This is a future optimization, not a blocker. Command hooks work correctly and the 30-second timeout provides headroom.
Claude Code: A plugin exists — AIC ships as a native Claude Code Plugin (integrations/claude/plugin/) via the Plugin Marketplace. For manual install, install.cjs, and MCP bootstrap, see §13.
Claude Code: Manual run or MCP bootstrap. The installer is integrations/claude/install.cjs in the repo — standalone, no dependency on mcp/src/. When the MCP server runs bootstrap, it prefers <project>/integrations/claude/install.cjs if that file exists under the opened workspace root; otherwise it runs the copy shipped inside the published @jatbas/aic package at package-relative integrations/claude/install.cjs (mirroring Cursor installer resolution in editor-integration-dispatch.ts).
node integrations/claude/install.cjs
The installer:
- Ensures
~/.claude/hooks/directory exists (resolve~/.claudefrom home). - For each hook script in
AIC_SCRIPT_NAMES: reads content fromintegrations/claude/hooks/and writes to~/.claude/hooks/only if content differs (idempotent). - Deletes any
aic-*.cjsfiles in~/.claude/hooks/that are not inAIC_SCRIPT_NAMES(stale script cleanup). - Reads
~/.claude/settings.json(if present) and merges AIC entries into existing config, preserving non-AIC entries; writes only if merged content differs. - Removes legacy project-local AIC hooks: when cwd is not the user home, deletes any
aic-*.cjsin.claude/hooks/and removes that directory if empty. For.claude/settings.local.json, strips thehookskey or deletes the file if nothing remains or the file is unparseable. - Writes the trigger rule (
.claude/CLAUDE.mdin the current working directory when writable), version-stamped; overwrites only when the installed version differs from the current package version.
The MCP server runs this installer when auto or forced bootstrap opens a Claude Code context
(detection per §2) and a resolved installer path exists (in-project or bundled). Triggers: on
workspace root listing (if the client supports roots) or on the first aic_compile for that
project. See installation.md for the user-facing path, including Cursor +
extension detection under ~/.cursor/extensions.
For end-user distribution, AIC is also packaged as a native Claude Code Plugin (integrations/claude/plugin/) installable via the Plugin Marketplace. See integrations/claude/plugin/ for the plugin structure.
Claude Code: .claude/CLAUDE.md is a fallback when hooks are disabled
(disableAllHooks: true). It tells the model to call aic_compile manually on every message.
With hooks active, the trigger rule becomes redundant — but it is kept because
users can disable all hooks and it costs nothing when hooks are running.
| Bug | Issue | Status | Workaround |
|---|---|---|---|
hookSpecificOutput JSON causes "UserPromptSubmit hook error" on first message of new session |
#17550 | Closed not_planned | Use plain text stdout for UserPromptSubmit and PreCompact (§6.1) |
SessionStart hook output silently discarded for new sessions in CLI |
#10373 | Open since Oct 2025 | Dual-path injection via UserPromptSubmit fallback (§7.2) |
| Plain text stdout caused "UserPromptSubmit hook error" in v2.0.69 (regression) | #13912 | Closed as duplicate of #12151 — resolved in later versions | No workaround needed; plain text now works |
Plugin hook hookSpecificOutput drops concurrent user hook flat additionalContext |
#31658 | Open Mar 2026 | Use consistent hookSpecificOutput format for events that require it; plain text for UserPromptSubmit |
All of the following must be verified for the Claude Code integration to be complete:
Context delivery:
-
aic-prompt-compile.cjsruns on UserPromptSubmit and passesintentandconversationId(transcript path, direct id, orresolveConversationIdFallback) toaic_compile(§7.1) -
aic-session-start.cjsinjects architectural invariants and project context viahookSpecificOutput(§7.2) -
aic-subagent-inject.cjsinjects context into subagents (§7.3)
Quality gate (Claude Code–specific):
-
aic-after-file-edit-tracker.cjsrecords edited files to temp file (§7.5) -
aic-stop-quality-check.cjsruns lint/typecheck, usesdecision: "block"when needed (§7.6) -
aic-block-no-verify.cjs/aic-inject-conversation-id.cjson PreToolUse (Bash + MCP) (§7.4)
Settings:
-
settings.json(or pluginhooks.json) has all nine top-level hook keys (includingSubagentStop) with correct matchers and options (§10)
Plugin and direct-install:
- Plugin path: the plugin provides hooks and MCP registration; direct installer path:
install.cjscopies to~/.claude/hooks/and merges into~/.claude/settings.json(§12, §13)
Temp file and marker conventions:
- Temp file
aic-edited-claude_code-<session_id>.jsonunderos.tmpdir()(sanitized key): written by PostToolUse (Edit|Write), read by Stop, cleaned by SessionEnd (integrations/shared/edited-files-cache.cjs) -
.aic/.session-context-injected: written by SessionStart (dual-path workaround), read by UserPromptSubmit, deleted by SessionEnd (§7.2)
integrations/claude/uninstall.cjs defaults to project-local cleanup: (unless --keep-project-artifacts) project aic.config.json, .aic/, matching ignore-file lines, and the AIC managed span in .claude/CLAUDE.md. It does not modify <project>/.cursor/; use the Cursor uninstall script for Cursor MCP and hooks. --global adds AIC entries in ~/.claude/settings.json, AIC scripts under ~/.claude/hooks/, and the same ~/.aic/ cleanup as the Cursor script (SQLite preserved unless --global --remove-database or env overrides). devMode: true in aic.config.json skips all changes unless --force. See installation.md § Uninstall for ordering, flags, and bundled paths inside @jatbas/aic.