diff --git a/AGENTS.md b/AGENTS.md index b4f5ee4..245ca6e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,7 +26,7 @@ This repository publishes the open-source server, console, integration catalog, - Resolve stable Kimi hooks through `$KIMI_CODE_HOME/config.toml` with `~/.kimi-code/config.toml` as the default, and treat an empty override as the default. Activate `~/.kimi/config.toml` only when the legacy home exists, ignore executable lookup as activation evidence, and collapse equal stable and legacy paths to the stable contract. Register exact `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` rules with ten-second commands; stable Kimi supports its current sixteen-event schema and legacy kimi-cli supports its thirteen-event schema. Preserve the native snake_case payload, propagate exit code `2`, and keep audit delivery fail-open with owner-only error logs. Use encoded PowerShell commands on Windows and exact two-argument shell commands on Unix. Commit every detected config, generated runtime, runtime config, and private key through one rollback-capable transaction. Status requires strict runtime metadata, a canonical private key, exact identity, and non-empty physical scripts. - Preserve Kimi TOML comments, array style, and unrelated formatting through syntax-preserving edits, then parse every selected document before writing any file. - Resolve Grok Build user hooks through `$GROK_HOME/hooks/*.json` with `~/.grok/hooks/*.json` as the default, and treat an empty override as the default. Keep Claude Code, Cursor, project `.grok/hooks`, plugin, and `hooks-paths` sources read-only. Register exact `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` groups with ten-second command handlers, preserve the complete native camelCase payload, and emit Grok's documented deny JSON plus exit code `2` for frozen or revoked agents. Use encoded PowerShell commands on Windows and exact two-argument shell commands on Unix. Commit each SDK's Grok hook file, generated runtimes, runtime config, and private key through one rollback-capable transaction. Status requires the exact event triple, strict runtime metadata, a canonical private key, exact identity, and non-empty physical scripts. -- Resolve Claude Code user hooks through `$CLAUDE_CONFIG_DIR/settings.json` with `~/.claude/settings.json` when the variable is absent. Match Claude Code's path resolution exactly: relative and empty values resolve from the current working directory, and literal tildes remain path segments. Keep project, local, managed, plugin, skill, and agent hook sources read-only. Register exact matchless `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` groups with ten-second exec-form command handlers. Preserve the complete native snake_case payload and propagate freeze and revocation through exit code `2`. Commit each SDK's Claude Code user settings, generated runtimes, runtime config, and private key through one rollback-capable transaction. Reject installation while the user source sets `disableAllHooks`, then require `/hooks` and `claude doctor` verification for higher-scope policy effects. +- Resolve Claude Code user hooks through `$CLAUDE_CONFIG_DIR/settings.json` with `~/.claude/settings.json` when the variable is absent. Match Claude Code's path resolution exactly: relative and empty values resolve from the current working directory, and literal tildes remain path segments. Keep project, local, managed, plugin, skill, and agent hook sources read-only. Validate the complete shipped handler schema, including command exec form, async rewake metadata, HTTP, MCP tool, prompt, and agent handlers. Register exact matchless `PreToolUse`, `PostToolUse`, and `PostToolUseFailure` groups with ten-second exec-form command handlers. Preserve the complete native snake_case payload and propagate freeze and revocation through exit code `2`. Commit each SDK's Claude Code user settings, generated runtimes, runtime config, and private key through one rollback-capable transaction. Reject installation while the user source sets `disableAllHooks`, then require `/hooks` and `claude doctor` verification for higher-scope policy effects. - Resolve Gemini CLI user hooks through `$GEMINI_CLI_HOME/.gemini/settings.json` with `~/.gemini/settings.json` when the variable is absent or empty. Preserve relative and literal-tilde home values as Gemini CLI does. Keep workspace, system defaults, system overrides, and extension hooks read-only. Preserve JSON comments while rejecting trailing commas and duplicate keys. Register exact matchless `BeforeTool` and `AfterTool` groups named `elydora-guard` and `elydora-audit` with ten-second command handlers. Preserve the complete native snake_case payload, emit valid JSON on exit code `0`, and propagate freeze and revocation through exit code `2`. Use encoded PowerShell commands on Windows. Commit each SDK's Gemini CLI user settings, generated runtimes, runtime config, and private key through one rollback-capable transaction. Respect `hooksConfig.enabled` and `hooksConfig.disabled`, then require `/hooks list` verification for higher-scope policy effects. - Write Auggie hooks only to `~/.augment/settings.json`; keep system and workspace settings read-only. Generate `.cmd` wrappers on Windows and `.sh` wrappers on Unix because Auggie dispatches supported script paths, and express hook timeouts in milliseconds. - Validate Auggie matcher syntax during installation with Node.js `new RegExp`; keep status and uninstall independent from the JavaScript validator so recovery remains available offline. diff --git a/sdks/node/src/plugins/claudecode-contract.ts b/sdks/node/src/plugins/claudecode-contract.ts index 21e97c9..9727343 100644 --- a/sdks/node/src/plugins/claudecode-contract.ts +++ b/sdks/node/src/plugins/claudecode-contract.ts @@ -22,7 +22,17 @@ const HOOK_EVENTS = new Set([ const COMMON_HANDLER_KEYS = ['if', 'once', 'statusMessage', 'timeout']; const HANDLER_KEYS: Readonly> = { - command: [...COMMON_HANDLER_KEYS, 'args', 'async', 'asyncRewake', 'command', 'shell', 'type'], + command: [ + ...COMMON_HANDLER_KEYS, + 'args', + 'async', + 'asyncRewake', + 'command', + 'rewakeMessage', + 'rewakeSummary', + 'shell', + 'type', + ], prompt: [...COMMON_HANDLER_KEYS, 'continueOnBlock', 'model', 'prompt', 'type'], agent: [...COMMON_HANDLER_KEYS, 'model', 'prompt', 'type'], http: [...COMMON_HANDLER_KEYS, 'allowedEnvVars', 'headers', 'type', 'url'], @@ -99,6 +109,10 @@ function optionalString(value: JsonObject, field: string, label: string): void { } } +function optionalNonEmptyString(value: JsonObject, field: string, label: string): void { + if (value[field] !== undefined) requireNonEmptyString(value[field], field, label); +} + function optionalBoolean(value: JsonObject, field: string, label: string): void { if (value[field] !== undefined && typeof value[field] !== 'boolean') { throw new Error(`${label} field "${field}" must be a boolean`); @@ -138,6 +152,8 @@ function validateHandler(value: unknown, event: string, group: number, index: nu if (value.args !== undefined) validateStringArray(value.args, 'args', label); optionalBoolean(value, 'async', label); optionalBoolean(value, 'asyncRewake', label); + optionalNonEmptyString(value, 'rewakeMessage', label); + optionalNonEmptyString(value, 'rewakeSummary', label); if (value.shell !== undefined && value.shell !== 'bash' && value.shell !== 'powershell') { throw new Error(`${label} field "shell" must be "bash" or "powershell"`); } diff --git a/sdks/node/test/claudecode-official.test.mjs b/sdks/node/test/claudecode-official.test.mjs index 4f9b27a..444448d 100644 --- a/sdks/node/test/claudecode-official.test.mjs +++ b/sdks/node/test/claudecode-official.test.mjs @@ -10,7 +10,22 @@ const claudeBinary = process.env.ELYDORA_CLAUDE_BINARY; test('official Claude Code accepts the installed user hook contract', { skip: claudeBinary ? false : 'set ELYDORA_CLAUDE_BINARY to the official Claude Code executable', }, async () => { - const fixture = await createFixture(); + const fixture = await createFixture({ + settings: { + hooks: { + Stop: [{ + hooks: [{ + type: 'command', + command: process.execPath, + args: ['--version'], + asyncRewake: true, + rewakeMessage: 'Background validation failed', + rewakeSummary: 'Validation feedback', + }], + }], + }, + }, + }); try { assert.equal((await fixture.install()).code, 0); const environment = { diff --git a/sdks/node/test/claudecode-plugin.test.mjs b/sdks/node/test/claudecode-plugin.test.mjs index abca808..ebcd7af 100644 --- a/sdks/node/test/claudecode-plugin.test.mjs +++ b/sdks/node/test/claudecode-plugin.test.mjs @@ -76,6 +76,15 @@ test('Claude installs one exact managed triple and preserves user settings', asy matcher: 'Bash', hooks: [{ type: 'command', command: 'existing-command', timeout: 5 }], }], + Stop: [{ + hooks: [{ + type: 'command', + command: 'background-check', + asyncRewake: true, + rewakeMessage: 'Background validation failed', + rewakeSummary: 'Validation feedback', + }], + }], }, }; const fixture = await createFixture({ settings: existing }); @@ -87,6 +96,7 @@ test('Claude installs one exact managed triple and preserves user settings', asy assert.equal(installed.settings.model, 'sonnet'); assert.deepEqual(installed.settings.hooks.Notification, existing.hooks.Notification); assert.deepEqual(installed.settings.hooks.PreToolUse[0], existing.hooks.PreToolUse[0]); + assert.deepEqual(installed.settings.hooks.Stop, existing.hooks.Stop); assertManagedTriple(installed.settings, fixture); const second = await fixture.install(); @@ -166,6 +176,8 @@ test('Claude rejects invalid official hook shapes before writes', async (t) => { ['command value', JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ type: 'command', command: '' }] }] } }), /non-empty string/i], ['argument type', JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ type: 'command', command: 'x', args: [1] }] }] } }), /array of strings/i], ['timeout zero', JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ type: 'command', command: 'x', timeout: 0 }] }] } }), /positive finite number/i], + ['empty rewake message', JSON.stringify({ hooks: { Stop: [{ hooks: [{ type: 'command', command: 'x', rewakeMessage: '' }] }] } }), /rewakeMessage.*non-empty string/i], + ['empty rewake summary', JSON.stringify({ hooks: { Stop: [{ hooks: [{ type: 'command', command: 'x', rewakeSummary: '' }] }] } }), /rewakeSummary.*non-empty string/i], ['headers type', JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ type: 'http', url: 'https://example.test', headers: { A: 1 } }] }] } }), /map names to strings/i], ['input type', JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ type: 'mcp_tool', server: 's', tool: 't', input: [] }] }] } }), /input.*must be an object/i], ];