diff --git a/docs/cli-reference.md b/docs/cli-reference.md
index 94fb8c8..53c20c4 100644
--- a/docs/cli-reference.md
+++ b/docs/cli-reference.md
@@ -154,7 +154,7 @@ npx charter adf init --module testing # add a single module to existing
- `--ai-dir
` — custom directory path (default: `.ai`). Resolved to an absolute path at runtime.
- `--force` — overwrite existing files. Without this flag, existing `.adf` files are skipped and reported; only the missing `manifest.adf` is written.
-- `--emit-pointers` — generate thin pointer files (`CLAUDE.md`, `.cursorrules`, `agents.md`)
+- `--emit-pointers` — generate thin pointer files (`CLAUDE.md`, `.cursorrules`, `agents.md`). The generated `CLAUDE.md` includes a `## Session Start` section with guidance to call the `charter_context` MCP tool at session start, giving agents the live constraint surface before any other action.
- `--module ` — add a single module to existing `.ai/` (delegates to `adf create`)
**Default scaffolding** (worker/frontend/backend/fullstack presets):
diff --git a/packages/cli/src/__tests__/adf-init.test.ts b/packages/cli/src/__tests__/adf-init.test.ts
index ce51e83..7c39f56 100644
--- a/packages/cli/src/__tests__/adf-init.test.ts
+++ b/packages/cli/src/__tests__/adf-init.test.ts
@@ -2,7 +2,7 @@ import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
-import { adfCommand } from '../commands/adf';
+import { adfCommand, POINTER_CLAUDE_MD, POINTER_CLAUDE_MD_HYBRID } from '../commands/adf';
const originalCwd = process.cwd();
const tempDirs: string[] = [];
@@ -80,6 +80,42 @@ describe('charter adf init — scaffolding guard', () => {
});
});
+describe('charter adf init --emit-pointers — CLAUDE.md Session Start section', () => {
+ it('POINTER_CLAUDE_MD contains a Session Start section', () => {
+ expect(POINTER_CLAUDE_MD).toContain('## Session Start');
+ expect(POINTER_CLAUDE_MD).toContain('charter_context');
+ });
+
+ it('POINTER_CLAUDE_MD_HYBRID contains a Session Start section', () => {
+ expect(POINTER_CLAUDE_MD_HYBRID).toContain('## Session Start');
+ expect(POINTER_CLAUDE_MD_HYBRID).toContain('charter_context');
+ });
+
+ it('POINTER_CLAUDE_MD Session Start section appears before Environment section', () => {
+ const sessionStartIdx = POINTER_CLAUDE_MD.indexOf('## Session Start');
+ const environmentIdx = POINTER_CLAUDE_MD.indexOf('## Environment');
+ expect(sessionStartIdx).toBeGreaterThan(-1);
+ expect(environmentIdx).toBeGreaterThan(-1);
+ expect(sessionStartIdx).toBeLessThan(environmentIdx);
+ });
+
+ it('POINTER_CLAUDE_MD_HYBRID Session Start section appears before Module Index section', () => {
+ const sessionStartIdx = POINTER_CLAUDE_MD_HYBRID.indexOf('## Session Start');
+ const moduleIndexIdx = POINTER_CLAUDE_MD_HYBRID.indexOf('## Module Index');
+ expect(sessionStartIdx).toBeGreaterThan(-1);
+ expect(moduleIndexIdx).toBeGreaterThan(-1);
+ expect(sessionStartIdx).toBeLessThan(moduleIndexIdx);
+ });
+
+ it('writes CLAUDE.md with Session Start section when --emit-pointers is used', async () => {
+ const tmp = makeTmp();
+ await adfCommand(DEFAULT_OPTIONS, ['init', '--emit-pointers']);
+ const claudeMd = fs.readFileSync(path.join(tmp, 'CLAUDE.md'), 'utf-8');
+ expect(claudeMd).toContain('## Session Start');
+ expect(claudeMd).toContain('charter_context');
+ });
+});
+
describe('charter serve startup — error discrimination', () => {
it('distinguishes missing .ai/ dir from missing manifest.adf in error message', () => {
// The distinction is tested indirectly via the error message text that
diff --git a/packages/cli/src/__tests__/hook.test.ts b/packages/cli/src/__tests__/hook.test.ts
index 7ace1bd..826f07f 100644
--- a/packages/cli/src/__tests__/hook.test.ts
+++ b/packages/cli/src/__tests__/hook.test.ts
@@ -3,7 +3,7 @@ import { execFileSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
-import { hookCommand } from '../commands/hook';
+import { hookCommand, printClaudeHookConfig } from '../commands/hook';
import type { CLIOptions } from '../index';
const baseOptions: CLIOptions = {
@@ -56,4 +56,38 @@ describe('hookCommand', () => {
const content = fs.readFileSync(hookPath, 'utf-8');
expect(content).toContain('echo "custom"');
});
+
+ it('hook print --claude returns 0 and outputs UserPromptSubmit config', async () => {
+ const logs: string[] = [];
+ vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
+ logs.push(args.map(String).join(' '));
+ });
+
+ const exitCode = await hookCommand(baseOptions, ['print', '--claude']);
+ expect(exitCode).toBe(0);
+
+ const output = logs.join('\n');
+ expect(output).toContain('UserPromptSubmit');
+ expect(output).toContain('charter context-refresh --once');
+ });
+
+ it('printClaudeHookConfig outputs valid JSON with correct shape', () => {
+ const logs: string[] = [];
+ vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
+ logs.push(args.map(String).join(' '));
+ });
+
+ printClaudeHookConfig();
+
+ const output = logs.join('\n');
+ const parsed = JSON.parse(output) as {
+ hooks: { UserPromptSubmit: Array<{ matcher: string; hooks: Array<{ type: string; command: string }> }> };
+ };
+ expect(parsed.hooks.UserPromptSubmit).toHaveLength(1);
+ expect(parsed.hooks.UserPromptSubmit[0].hooks[0].command).toBe('charter context-refresh --once');
+ });
+
+ it('hook print without --claude throws', async () => {
+ await expect(hookCommand(baseOptions, ['print'])).rejects.toThrow('hook print requires --claude');
+ });
});
diff --git a/packages/cli/src/commands/adf.ts b/packages/cli/src/commands/adf.ts
index 4792b6a..568a339 100644
--- a/packages/cli/src/commands/adf.ts
+++ b/packages/cli/src/commands/adf.ts
@@ -269,6 +269,12 @@ export const POINTER_CLAUDE_MD = `# CLAUDE.md
> New rules should be added to the appropriate \`.ai/*.adf\` module.
> See \`.ai/manifest.adf\` for the module routing manifest.
+## Session Start
+
+Call \`charter_context\` (MCP tool) before any other action. It returns the live constraint surface — open work, recent decisions, and metric ceilings — so you act within current project state rather than reconstructing it from scratch.
+
+If \`charter serve\` is not running, read \`.ai/context.snapshot.json\` directly or run \`charter context-refresh\` to generate it.
+
## Environment
`;
@@ -280,6 +286,12 @@ export const POINTER_CLAUDE_MD_HYBRID = `# CLAUDE.md
> New rules should be added to the appropriate \`.ai/*.adf\` module.
> See \`.ai/manifest.adf\` for the module routing manifest.
+## Session Start
+
+Call \`charter_context\` (MCP tool) before any other action. It returns the live constraint surface — open work, recent decisions, and metric ceilings — so you act within current project state rather than reconstructing it from scratch.
+
+If \`charter serve\` is not running, read \`.ai/context.snapshot.json\` directly or run \`charter context-refresh\` to generate it.
+
## Module Index
diff --git a/packages/cli/src/commands/bootstrap.ts b/packages/cli/src/commands/bootstrap.ts
index 0ff9302..1085955 100644
--- a/packages/cli/src/commands/bootstrap.ts
+++ b/packages/cli/src/commands/bootstrap.ts
@@ -364,6 +364,12 @@ export async function bootstrapCommand(options: CLIOptions, args: string[]): Pro
});
}
+ result.nextSteps.push({
+ cmd: 'charter hook print --claude # paste output into .claude/settings.json → hooks.UserPromptSubmit',
+ required: false,
+ reason: 'Auto-refresh context at session start so charter_context returns live state, not a cold snapshot, before the agent acts',
+ });
+
// ========================================================================
// Governance Gaps — surface what's configured but not enforced
// ========================================================================
diff --git a/packages/cli/src/commands/hook.ts b/packages/cli/src/commands/hook.ts
index 397784e..5865dd6 100644
--- a/packages/cli/src/commands/hook.ts
+++ b/packages/cli/src/commands/hook.ts
@@ -96,14 +96,40 @@ if [ -f ".ai/manifest.adf" ]; then
fi
`;
+// Charter cannot write to .claude/settings.json safely — it's user-controlled.
+// `print --claude` emits the config snippet for the user to paste instead.
+const CLAUDE_SESSION_HOOK_CONFIG = {
+ hooks: {
+ UserPromptSubmit: [
+ {
+ matcher: '.*',
+ hooks: [{ type: 'command', command: 'charter context-refresh --once' }],
+ },
+ ],
+ },
+};
+
+export function printClaudeHookConfig(): void {
+ console.log(JSON.stringify(CLAUDE_SESSION_HOOK_CONFIG, null, 2));
+}
+
export async function hookCommand(options: CLIOptions, args: string[]): Promise {
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
printHelp();
return EXIT_CODE.SUCCESS;
}
+ if (args[0] === 'print') {
+ const wantClaude = args.includes('--claude');
+ if (!wantClaude) {
+ throw new CLIError('hook print requires --claude.');
+ }
+ printClaudeHookConfig();
+ return EXIT_CODE.SUCCESS;
+ }
+
if (args[0] !== 'install') {
- throw new CLIError(`Unknown hook subcommand: ${args[0]}. Supported: install`);
+ throw new CLIError(`Unknown hook subcommand: ${args[0]}. Supported: install, print`);
}
const wantCommitMsg = args.includes('--commit-msg');
@@ -240,6 +266,7 @@ function printHelp(): void {
console.log(' Usage:');
console.log(' charter hook install --commit-msg [--force]');
console.log(' charter hook install --pre-commit [--force]');
+ console.log(' charter hook print --claude');
console.log('');
console.log(' --commit-msg: Install a git commit-msg hook that normalizes Governed-By and');
console.log(' Resolves-Request trailers using git interpret-trailers.');
@@ -249,4 +276,8 @@ function printHelp(): void {
console.log(' Vendor file bloat is extracted, routed to .adf modules, and re-staged.');
console.log(' Skip tidy with CHARTER_SKIP_TIDY=1. Only gates when .ai/manifest.adf exists.');
console.log('');
+ console.log(' print --claude: Print the Claude Code session hook config snippet to stdout.');
+ console.log(' Paste into .claude/settings.json → hooks.UserPromptSubmit to auto-refresh');
+ console.log(' context at session start so charter_context returns live state.');
+ console.log('');
}
diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts
index 723c35f..23a9276 100644
--- a/packages/cli/src/index.ts
+++ b/packages/cli/src/index.ts
@@ -67,6 +67,7 @@ Usage:
Install git commit-msg hook for trailer normalization
charter hook install --pre-commit [--force]
Install git pre-commit hook for ADF evidence gate
+ charter hook print --claude Print Claude Code session hook config (paste into .claude/settings.json)
charter adf ADF context format tools (init, fmt, patch, create, bundle, sync, evidence, migrate, metrics)
charter serve [--name ] [--ai-dir ]
Expose ADF project context as an MCP server (stdio, for Claude Code/Codex/Cursor)