Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
70 changes: 42 additions & 28 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,41 +20,53 @@ 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 {
return isDotcontextHookDispatchCommand(command, 'claude-code');
}

export function isCurrentClaudeCodeHookCommand(command: unknown): boolean {
return isCurrentDotcontextHookDispatchCommand(command, 'claude-code');
export function isCurrentClaudeCodeHookCommand(
command: unknown,
options?: ResolveHookDispatchCommandOptions
): boolean {
return isCurrentDotcontextHookDispatchCommand(command, 'claude-code', options);
}
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';
Loading
Loading