From 49571365a8e2271d55132979b4af4b6ad11805f3 Mon Sep 17 00:00:00 2001 From: Abdul Wahab Date: Mon, 24 Aug 2026 04:26:56 +0500 Subject: [PATCH] Fix: rewrite Grok project hooks to the global skill path (#642) Grok was skipped by the hook-command rewrite, so a global skill install left .grok/hooks/impeccable.json pointing at a project-relative hook.mjs that does not exist. AI assistance: Cursor Grok 4.6 implemented this change. Co-authored-by: Cursor --- cli/bin/commands/skills.mjs | 6 +++--- tests/skills-cli.test.js | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/cli/bin/commands/skills.mjs b/cli/bin/commands/skills.mjs index c5f5af5cb..edbf56790 100644 --- a/cli/bin/commands/skills.mjs +++ b/cli/bin/commands/skills.mjs @@ -1402,7 +1402,7 @@ function hookScriptPathForProvider(skillRoot, provider) { if (provider === '.cursor') { return join(skillRoot, provider, 'skills', 'impeccable', 'scripts', 'hook-before-edit.mjs'); } - if (provider === '.claude' || provider === '.agents') { + if (provider === '.claude' || provider === '.agents' || provider === '.grok') { return join(skillRoot, provider, 'skills', 'impeccable', 'scripts', 'hook.mjs'); } return null; @@ -1478,8 +1478,8 @@ function guardHookCommand(quotedPath, provider) { // entries additionally get a `commandWindows` sibling for cmd.exe. function rewriteHookCommandsForSkillRoot(value, provider, { skillRoot, absolute }) { const hookScript = hookScriptPathForProvider(skillRoot, provider); - // Providers we don't own a `node "PATH"` command hook for (.github, .grok) - // carry their own portable command forms; leave them untouched. + // Providers we don't own a `node "PATH"` command hook for (.github) carry + // their own portable command forms; leave them untouched. if (!hookScript) return value; // Project-scope installs derive the provider's own project-relative path diff --git a/tests/skills-cli.test.js b/tests/skills-cli.test.js index f78f3156b..1156d3dc2 100644 --- a/tests/skills-cli.test.js +++ b/tests/skills-cli.test.js @@ -108,6 +108,12 @@ function createFakeUniversalBundle(root, providers = ['.claude', '.agents', '.cu hooks: { PostToolUse: [{ matcher: 'apply_patch', hooks: [{ type: 'command', command: 'node ".codex/skills/impeccable/scripts/hook.mjs"' }] }] }, }, null, 2)); } + if (providers.includes('.grok')) { + mkdirSync(join(bundleRoot, '.grok', 'hooks'), { recursive: true }); + writeFileSync(join(bundleRoot, '.grok', 'hooks', 'impeccable.json'), JSON.stringify({ + hooks: { PostToolUse: [{ matcher: 'Edit|Write|MultiEdit', hooks: [{ type: 'command', command: 'node ".grok/skills/impeccable/scripts/hook.mjs"' }] }] }, + }, null, 2)); + } // Native subagent definitions, mirroring the build's provider agents output. if (providers.includes('.github')) { mkdirSync(join(bundleRoot, '.github', 'agents'), { recursive: true }); @@ -1125,21 +1131,24 @@ describe('skills install/update: local universal bundle e2e', () => { const tmp = mkdtempSync(join(tmpdir(), 'imp-test-scope-user-hooks-')); const home = mkdtempSync(join(tmpdir(), 'imp-home-scope-user-hooks-')); execSync('git init', { cwd: tmp }); - const bundleRoot = createFakeUniversalBundle(tmp, ['.claude', '.agents', '.cursor']); + const bundleRoot = createFakeUniversalBundle(tmp, ['.claude', '.agents', '.cursor', '.grok']); - const output = run('skills install -y --providers=claude,codex,cursor --scope=global', { + const output = run('skills install -y --providers=claude,codex,cursor,grok --scope=global', { cwd: tmp, env: { ...process.env, HOME: home, IMPECCABLE_BUNDLE_PATH: bundleRoot }, }); - expect(output).toContain('Installed impeccable into: .claude, .agents, .cursor (global)'); - for (const provider of ['.claude', '.agents', '.cursor']) { + expect(output).toContain('Installed impeccable into: .claude, .agents, .cursor, .grok (global)'); + for (const provider of ['.claude', '.agents', '.cursor', '.grok']) { expect(existsSync(join(home, provider, 'skills', 'impeccable', 'SKILL.md'))).toBe(true); expect(existsSync(join(tmp, provider, 'skills', 'impeccable', 'SKILL.md'))).toBe(false); } expect(readFileSync(join(tmp, '.claude', 'settings.local.json'), 'utf8')).toContain(join(home, '.claude', 'skills', 'impeccable', 'scripts', 'hook.mjs')); expect(readFileSync(join(tmp, '.codex', 'hooks.json'), 'utf8')).toContain(join(home, '.agents', 'skills', 'impeccable', 'scripts', 'hook.mjs')); expect(readFileSync(join(tmp, '.cursor', 'hooks.json'), 'utf8')).toContain(join(home, '.cursor', 'skills', 'impeccable', 'scripts', 'hook-before-edit.mjs')); + const grokHooks = readFileSync(join(tmp, '.grok', 'hooks', 'impeccable.json'), 'utf8'); + expect(grokHooks).toContain(join(home, '.grok', 'skills', 'impeccable', 'scripts', 'hook.mjs')); + expect(grokHooks).not.toContain('".grok/skills/impeccable/scripts/hook.mjs"'); rmSync(tmp, { recursive: true, force: true }); rmSync(home, { recursive: true, force: true });