Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,8 +434,8 @@ npx -y @dotcontext/cli@latest hook doctor codex --json

| Host | Config | Dispatch |
| --- | --- | --- |
| `claude-code` | `.claude/settings.json` | `npx -y @dotcontext/cli@latest hook dispatch --source claude-code` |
| `codex` | `.codex/hooks.json` or inline in `.codex/config.toml` | `npx -y @dotcontext/cli@latest hook dispatch --source codex` |
| `claude-code` | `.claude/settings.json` | `dotcontext hook dispatch --source claude-code` (falls back to `npx -y @dotcontext/cli@<installed version> ...` when no global binary is on PATH) |
| `codex` | `.codex/hooks.json` or inline in `.codex/config.toml` | `dotcontext hook dispatch --source codex` (same fallback) |
| `pi` | `pi install npm:@dotcontext/pi` | In-process TypeScript extension |

Codex-specific activation step:
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/en/about/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ This is the surface an AI client (Claude Code, Cursor, Windsurf, and others) tal

### The integrations boundary

**Integrations** connect host lifecycle events to the harness without going through MCP. Claude Code and Codex CLI use shell dispatch (`npx -y @dotcontext/cli@latest hook dispatch`); Pi loads the `@dotcontext/pi` npm extension in-process.
**Integrations** connect host lifecycle events to the harness without going through MCP. Claude Code and Codex CLI use shell dispatch (`dotcontext hook dispatch`, with a version-pinned npx fallback when no global binary is on PATH); Pi loads the `@dotcontext/pi` npm extension in-process.

Integrations call the harness only — they never import `cli` or `mcp`. See [using dotcontext with hooks](/guides/using-with-hooks/) and [using dotcontext with Pi](/guides/using-with-pi/).

Expand Down
12 changes: 9 additions & 3 deletions docs/src/content/docs/en/guides/using-with-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,16 @@ Repeated trace append failures are recorded under `.context/runtime/hooks/trace-

## Claude Code

The installer writes `hooks` entries to Claude Code settings. Each entry runs:
The installer writes `hooks` entries to Claude Code settings. When a global `dotcontext` binary is on PATH, each entry runs it directly; otherwise the entry falls back to npx pinned to the installed CLI version:

```bash
npx -y @dotcontext/cli@latest hook dispatch --source claude-code
dotcontext hook dispatch --source claude-code
# or, when no global binary is available:
npx -y @dotcontext/cli@<installed version> hook dispatch --source claude-code
```

Running the binary directly (or a pinned version) avoids re-resolving the npm `latest` tag on every SessionStart, PostToolUse, and Stop event.

Wired events (v1):

| Event | Matcher |
Expand All @@ -119,7 +123,9 @@ Start a Claude Code session in a repository with `.context/` initialized and con
Codex hooks use the same dispatch command with `--source codex`:

```bash
npx -y @dotcontext/cli@latest hook dispatch --source codex
dotcontext hook dispatch --source codex
# or, when no global binary is available:
npx -y @dotcontext/cli@<installed version> hook dispatch --source codex
```

The installer writes either:
Expand Down
11 changes: 8 additions & 3 deletions docs/src/content/docs/pt-br/guides/hook-session-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,18 @@ Pi extension
-> resposta in-process para Pi
```

Para Claude Code e Codex CLI, o instalador escreve comandos shell que chamam:
Para Claude Code e Codex CLI, o instalador escreve comandos shell que preferem o binário global `dotcontext` quando presente no PATH, com fallback para npx pinado na versão instalada:

```bash
npx -y @dotcontext/cli@latest hook dispatch --source claude-code
npx -y @dotcontext/cli@latest hook dispatch --source codex
dotcontext hook dispatch --source claude-code
dotcontext hook dispatch --source codex
# ou, quando não há binário global disponível:
npx -y @dotcontext/cli@<versão instalada> hook dispatch --source claude-code
npx -y @dotcontext/cli@<versão instalada> hook dispatch --source codex
```

Executar o binário direto (ou uma versão pinada) evita resolver a tag `latest` do npm a cada evento de SessionStart, PostToolUse e Stop.

Por padrão, a instalação de hooks é no projeto atual:

```bash
Expand Down
8 changes: 6 additions & 2 deletions docs/src/content/docs/pt-br/guides/using-with-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ Falhas repetidas de append trace são registradas em `.context/runtime/hooks/tra
O instalador grava entradas `hooks` em `.claude/settings.json` por padrão. Cada entrada chama:

```bash
npx -y @dotcontext/cli@latest hook dispatch --source claude-code
dotcontext hook dispatch --source claude-code
# ou, quando não há binário global disponível:
npx -y @dotcontext/cli@<versão instalada> hook dispatch --source claude-code
```

Eventos configurados:
Expand All @@ -98,7 +100,9 @@ Depois de instalar, reinicie o Claude Code. Em um projeto com `.context/` inicia
Hooks do Codex usam o mesmo dispatch com `--source codex`:

```bash
npx -y @dotcontext/cli@latest hook dispatch --source codex
dotcontext hook dispatch --source codex
# ou, quando não há binário global disponível:
npx -y @dotcontext/cli@<versão instalada> hook dispatch --source codex
```

O instalador escreve uma destas configurações:
Expand Down
5 changes: 3 additions & 2 deletions src/cli/services/__tests__/hookInstallService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
resolveHookInstallHostSelection,
} from '../hookInstallService';
import { CODEX_HOOK_TRUST_REMINDER } from '../../../integrations/codex';
import { buildHookDispatchCommand } from '../../../integrations/shared';
import type { CLIInterface } from '../../../utils/cliUI';

const createMockUI = (): CLIInterface => ({
Expand Down Expand Up @@ -138,7 +139,7 @@ describe('HookInstallService', () => {

const command = config.hooks.SessionStart[0].hooks[0];
expect(command.type).toBe('command');
expect(command.command).toContain('npx -y @dotcontext/cli@latest hook dispatch --source claude-code');
expect(command.command).toBe(buildHookDispatchCommand('claude-code'));
});

it('supports dry-run mode for Claude Code', async () => {
Expand Down Expand Up @@ -214,7 +215,7 @@ describe('HookInstallService', () => {
expect(config).toContain('[features]');
expect(config).toContain('hooks = true');
expect(config).toContain('[[hooks.SessionStart]]');
expect(config).toContain('npx -y @dotcontext/cli@latest hook dispatch --source codex');
expect(config).toContain(buildHookDispatchCommand('codex'));
});

it('prints Pi instructions and writes .mcp.json snippet', async () => {
Expand Down
19 changes: 10 additions & 9 deletions src/integrations/__tests__/hookInstallServices.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
previewCodexHooks,
} from '../codex';
import {
buildHookDispatchCommand,
normalizeToolEvent,
resolveHarnessHookFromHostEvent,
} from '../shared';
Expand Down Expand Up @@ -132,7 +133,7 @@ describe('hook install services', () => {
const written = await fs.readFile(configPath, 'utf8');
expect(written).toContain('[features]');
expect(written).toContain('hooks = true');
expect(written).toContain('npx -y @dotcontext/cli@latest hook dispatch --source codex');
expect(written).toContain(buildHookDispatchCommand('codex'));
expect(written).toContain('[mcp_servers.dotcontext]');
});

Expand Down Expand Up @@ -192,9 +193,9 @@ describe('hook install services', () => {
expect(result.action).toBe('skipped');
});

it('upgrades legacy Claude Code hook commands to the current npx dispatch command', async () => {
it('upgrades legacy Claude Code hook commands to the current dispatch command', async () => {
const configPath = path.join(tempDir, '.claude', 'settings.json');
const legacyCommand = 'dotcontext hook dispatch --source claude-code';
const legacyCommand = 'npx -y @dotcontext/cli@latest hook dispatch --source claude-code';
await fs.outputJson(configPath, {
hooks: {
SessionStart: [{ hooks: [{ type: 'command', command: legacyCommand }] }],
Expand All @@ -211,19 +212,19 @@ describe('hook install services', () => {
expect(result.action).toBe('updated');

const written = await fs.readJson(configPath);
expect(written.hooks.SessionStart[0].hooks[0].command).toContain(
'npx -y @dotcontext/cli@latest hook dispatch --source claude-code'
expect(written.hooks.SessionStart[0].hooks[0].command).toBe(
buildHookDispatchCommand('claude-code')
);
});

it('upgrades legacy Codex TOML hook commands to the current npx dispatch command', async () => {
it('upgrades legacy Codex TOML hook commands to the current dispatch command', async () => {
const configPath = path.join(tempDir, '.codex', 'config.toml');
await fs.outputFile(
configPath,
[
'[[hooks.SessionStart]]',
'matcher = "*"',
'command = "dotcontext hook dispatch --source codex"',
'command = "npx -y @dotcontext/cli@latest hook dispatch --source codex"',
'',
].join('\n')
);
Expand All @@ -237,8 +238,8 @@ describe('hook install services', () => {
expect(result.action).toBe('updated');

const written = await fs.readFile(configPath, 'utf8');
expect(written).toContain('npx -y @dotcontext/cli@latest hook dispatch --source codex');
expect(written).not.toContain('command = "dotcontext hook dispatch --source codex"');
expect(written).toContain(buildHookDispatchCommand('codex'));
expect(written).not.toContain('@latest');
});

it('previews Codex TOML append output', async () => {
Expand Down
63 changes: 37 additions & 26 deletions src/integrations/claude-code/hooks/claudeCodeHookTemplates.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {
buildHookDispatchCommand,
CLAUDE_CODE_HOOK_DISPATCH_COMMAND,
isCurrentDotcontextHookDispatchCommand,
isDotcontextHookDispatchCommand,
type ResolveHookDispatchCommandOptions,
} from '../../shared/hookDispatchCommands';

export { CLAUDE_CODE_HOOK_DISPATCH_COMMAND };
Expand All @@ -18,35 +20,44 @@ export interface ClaudeCodeHookMatcherEntry {

export type ClaudeCodeHookTemplate = ClaudeCodeHookMatcherEntry[];

export function buildClaudeCodeHookTemplates(
command: string = buildHookDispatchCommand('claude-code')
): Record<'SessionStart' | 'PostToolUse' | 'Stop', ClaudeCodeHookTemplate> {
return {
SessionStart: [
{
matcher: '*',
hooks: [{ type: 'command', command }],
},
],
PostToolUse: [
{
matcher: '^Write$|^Edit$|^Bash$',
hooks: [{ type: 'command', command }],
},
],
Stop: [
{
hooks: [{ type: 'command', command }],
},
],
};
}

/**
* Static template shape built with the pinned npx command. Prefer
* buildClaudeCodeHookTemplates() when writing configs so the command can
* resolve to the local dotcontext binary when it is available.
*/
export const CLAUDE_CODE_HOOK_TEMPLATES: Record<
'SessionStart' | 'PostToolUse' | 'Stop',
ClaudeCodeHookTemplate
> = {
SessionStart: [
{
matcher: '*',
hooks: [{ type: 'command', command: CLAUDE_CODE_HOOK_DISPATCH_COMMAND }],
},
],
PostToolUse: [
{
matcher: '^Write$|^Edit$|^Bash$',
hooks: [{ type: 'command', command: CLAUDE_CODE_HOOK_DISPATCH_COMMAND }],
},
],
Stop: [
{
hooks: [{ type: 'command', command: CLAUDE_CODE_HOOK_DISPATCH_COMMAND }],
},
],
};

export function buildClaudeCodeHooksFragment(): Record<string, ClaudeCodeHookTemplate> {
return {
SessionStart: CLAUDE_CODE_HOOK_TEMPLATES.SessionStart,
PostToolUse: CLAUDE_CODE_HOOK_TEMPLATES.PostToolUse,
Stop: CLAUDE_CODE_HOOK_TEMPLATES.Stop,
};
> = buildClaudeCodeHookTemplates(CLAUDE_CODE_HOOK_DISPATCH_COMMAND);

export function buildClaudeCodeHooksFragment(
options?: ResolveHookDispatchCommandOptions
): Record<string, ClaudeCodeHookTemplate> {
return buildClaudeCodeHookTemplates(buildHookDispatchCommand('claude-code', options));
}

export function isDotcontextClaudeCodeHookCommand(command: unknown): boolean {
Expand Down
1 change: 1 addition & 0 deletions src/integrations/claude-code/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ export {
export {
CLAUDE_CODE_HOOK_TEMPLATES,
CLAUDE_CODE_HOOK_DISPATCH_COMMAND,
buildClaudeCodeHookTemplates,
buildClaudeCodeHooksFragment,
} from './claudeCodeHookTemplates';
78 changes: 48 additions & 30 deletions src/integrations/codex/hooks/codexHookTemplates.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {
buildHookDispatchCommand,
CODEX_HOOK_DISPATCH_COMMAND,
isCurrentDotcontextHookDispatchCommand,
isDotcontextHookDispatchCommand,
type ResolveHookDispatchCommandOptions,
} from '../../shared/hookDispatchCommands';

export { CODEX_HOOK_DISPATCH_COMMAND };
Expand All @@ -18,40 +20,51 @@ export interface CodexHookMatcherEntry {

export type CodexHookTemplate = CodexHookMatcherEntry[];

export function buildCodexHookTemplates(
command: string = buildHookDispatchCommand('codex')
): Record<'SessionStart' | 'PostToolUse' | 'Stop', CodexHookTemplate> {
return {
SessionStart: [
{
matcher: '*',
hooks: [{ type: 'command', command }],
},
],
PostToolUse: [
{
matcher: '^Write$|^Edit$|^Bash$',
hooks: [{ type: 'command', command }],
},
],
Stop: [
{
matcher: '*',
hooks: [{ type: 'command', command }],
},
],
};
}

/**
* Static template shape built with the pinned npx command. Prefer
* buildCodexHookTemplates() when writing configs so the command can resolve
* to the local dotcontext binary when it is available.
*/
export const CODEX_HOOK_TEMPLATES: Record<
'SessionStart' | 'PostToolUse' | 'Stop',
CodexHookTemplate
> = {
SessionStart: [
{
matcher: '*',
hooks: [{ type: 'command', command: CODEX_HOOK_DISPATCH_COMMAND }],
},
],
PostToolUse: [
{
matcher: '^Write$|^Edit$|^Bash$',
hooks: [{ type: 'command', command: CODEX_HOOK_DISPATCH_COMMAND }],
},
],
Stop: [
{
matcher: '*',
hooks: [{ type: 'command', command: CODEX_HOOK_DISPATCH_COMMAND }],
},
],
};

export function buildCodexHooksFragment(): Record<string, CodexHookTemplate> {
return {
SessionStart: CODEX_HOOK_TEMPLATES.SessionStart,
PostToolUse: CODEX_HOOK_TEMPLATES.PostToolUse,
Stop: CODEX_HOOK_TEMPLATES.Stop,
};
> = buildCodexHookTemplates(CODEX_HOOK_DISPATCH_COMMAND);

export function buildCodexHooksFragment(
options?: ResolveHookDispatchCommandOptions
): Record<string, CodexHookTemplate> {
return buildCodexHookTemplates(buildHookDispatchCommand('codex', options));
}

export function buildCodexHooksDocument(): { hooks: Record<string, CodexHookTemplate> } {
return { hooks: buildCodexHooksFragment() };
export function buildCodexHooksDocument(
options?: ResolveHookDispatchCommandOptions
): { hooks: Record<string, CodexHookTemplate> } {
return { hooks: buildCodexHooksFragment(options) };
}

export function isDotcontextCodexHookCommand(command: unknown): boolean {
Expand All @@ -64,6 +77,7 @@ export function isCurrentCodexHookCommand(command: unknown): boolean {

export interface BuildCodexTomlHookBlocksOptions {
includeFeatures?: boolean;
command?: ResolveHookDispatchCommandOptions;
}
Comment on lines 81 to 84

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to resolveOptions in d576133.


export function buildCodexTomlHookBlocks(
Expand All @@ -75,7 +89,11 @@ export function buildCodexTomlHookBlocks(
lines.push('[features]', 'hooks = true', '');
}

for (const [eventName, entries] of Object.entries(CODEX_HOOK_TEMPLATES)) {
const templates = buildCodexHookTemplates(
buildHookDispatchCommand('codex', options.command)
);

for (const [eventName, entries] of Object.entries(templates)) {
for (const entry of entries) {
lines.push(`[[hooks.${eventName}]]`);
if (entry.matcher) {
Expand Down
1 change: 1 addition & 0 deletions src/integrations/codex/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export {
CODEX_HOOK_TEMPLATES,
CODEX_HOOK_DISPATCH_COMMAND,
CODEX_HOOK_TRUST_REMINDER,
buildCodexHookTemplates,
buildCodexHooksDocument,
buildCodexTomlHookBlocks,
} from './codexHookTemplates';
Loading
Loading