Summary
When running the standalone CLI (node dist/cli.js), hook installation writes working hook entries into ~/.claude/settings.json, but the referenced hook script is never actually copied to ~/.pixel-agents/hooks/claude-hook.js — because copyHookScript() is called with the wrong base path, doubling the dist segment. Every Claude Code hook event on the machine then fails silently (node: cannot find module) until the file is manually placed, and the CLI logs a false "Hooks installed" success right after logging the actual failure.
Steps to reproduce
git clone https://github.com/pixel-agents-hq/pixel-agents.git
cd pixel-agents
npm install
npm run build
node dist/cli.js --port 3100
Observed log output
[Pixel Agents] Hooks installed in ~/.claude/settings.json
[Pixel Agents] Hook script not found at /path/to/pixel-agents/dist/dist/hooks/claude-hook.js
[Pixel Agents] Hooks installed
Note the double dist/dist in the "not found" path, and that the CLI reports success (Hooks installed) immediately after reporting the failure.
$ ls ~/.pixel-agents/hooks/
# empty
$ cat ~/.claude/settings.json | jq '.hooks.Stop'
[
{
"matcher": "",
"hooks": [{ "type": "command", "command": "node \"/home/USER/.pixel-agents/hooks/claude-hook.js\"", "timeout": 5 }]
}
]
So every hook event fired by Claude Code (globally, for every project) invokes a script that doesn't exist.
Root cause
server/src/cli.ts:
const distRoot = __dirname; // already == .../dist (line 63)
...
copyHookScript(distRoot); // lines 110, 138
server/src/providers/hook/claude/claudeHookInstaller.ts:
export function copyHookScript(extensionPath: string): void {
const src = path.join(extensionPath, 'dist', 'hooks', CLAUDE_HOOK_SCRIPT_NAME); // line 172
...
copyHookScript assumes its argument is the package/extension root and appends dist/hooks/... itself — correct for the VS Code adapter, which calls it with this.context.extensionPath (the extension root, not including dist/) in adapters/vscode/PixelAgentsViewProvider.ts. But cli.ts passes distRoot, which is __dirname of the already-compiled dist/cli.js — i.e. already inside dist/. The result is a lookup at <root>/dist/dist/hooks/claude-hook.js, which never exists, so the fs.existsSync(src) check in copyHookScript fails and it console.warns and returns without copying — while the caller in cli.ts has no way to detect that failure and unconditionally logs "[Pixel Agents] Hooks installed" right after.
Expected behavior
copyHookScript should resolve the hook script relative to the actual dist/hooks/ next to cli.js, e.g. copyHookScript(path.dirname(distRoot)) from cli.ts (undoing the extra dist join), or better: give copyHookScript an explicit "already the dist dir" mode, since it's now called from two different path conventions (extension root vs. compiled-dist root).
- The CLI should not print "Hooks installed" (or should print a warning) when
copyHookScript failed to find/copy the source file — copyHookScript currently swallows this as console.warn with no return value the caller can check.
Impact
This silently breaks all Claude Code hooks (SessionStart, Stop, PreToolUse, PostToolUse, Notification, etc.) machine-wide for anyone who runs the standalone CLI from a source build, with no visible error beyond a log line that's easy to miss between two lines both saying "installed".
Suggested fix
// claudeHookInstaller.ts
export function copyHookScript(hookScriptsSourceDir: string): boolean {
const src = path.join(hookScriptsSourceDir, CLAUDE_HOOK_SCRIPT_NAME);
...
if (!fs.existsSync(src)) {
console.warn(`[Pixel Agents] Hook script not found at ${src}`);
return false;
}
fs.copyFileSync(src, dst);
fs.chmodSync(dst, 0o700);
return true;
}
// cli.ts
const ok = copyHookScript(path.join(distRoot, 'hooks'));
console.log(ok ? '[Pixel Agents] Hooks installed' : '[Pixel Agents] Hooks NOT installed — hook script missing');
// PixelAgentsViewProvider.ts
copyHookScript(path.join(this.context.extensionPath, 'dist', 'hooks'));
(i.e. make the parameter always mean "directory containing the hook script", removing the ambiguity between the two call sites.)
Environment
- OS: Ubuntu 24.04.4 LTS
- Node: v22.0.0
- Built from source at current
main
Summary
When running the standalone CLI (
node dist/cli.js), hook installation writes working hook entries into~/.claude/settings.json, but the referenced hook script is never actually copied to~/.pixel-agents/hooks/claude-hook.js— becausecopyHookScript()is called with the wrong base path, doubling thedistsegment. Every Claude Code hook event on the machine then fails silently (node: cannot find module) until the file is manually placed, and the CLI logs a false "Hooks installed" success right after logging the actual failure.Steps to reproduce
git clone https://github.com/pixel-agents-hq/pixel-agents.git cd pixel-agents npm install npm run build node dist/cli.js --port 3100Observed log output
Note the double
dist/distin the "not found" path, and that the CLI reports success (Hooks installed) immediately after reporting the failure.So every hook event fired by Claude Code (globally, for every project) invokes a script that doesn't exist.
Root cause
server/src/cli.ts:server/src/providers/hook/claude/claudeHookInstaller.ts:copyHookScriptassumes its argument is the package/extension root and appendsdist/hooks/...itself — correct for the VS Code adapter, which calls it withthis.context.extensionPath(the extension root, not includingdist/) inadapters/vscode/PixelAgentsViewProvider.ts. Butcli.tspassesdistRoot, which is__dirnameof the already-compileddist/cli.js— i.e. already insidedist/. The result is a lookup at<root>/dist/dist/hooks/claude-hook.js, which never exists, so thefs.existsSync(src)check incopyHookScriptfails and itconsole.warns and returns without copying — while the caller incli.tshas no way to detect that failure and unconditionally logs"[Pixel Agents] Hooks installed"right after.Expected behavior
copyHookScriptshould resolve the hook script relative to the actualdist/hooks/next tocli.js, e.g.copyHookScript(path.dirname(distRoot))fromcli.ts(undoing the extradistjoin), or better: givecopyHookScriptan explicit "already the dist dir" mode, since it's now called from two different path conventions (extension root vs. compiled-dist root).copyHookScriptfailed to find/copy the source file —copyHookScriptcurrently swallows this asconsole.warnwith no return value the caller can check.Impact
This silently breaks all Claude Code hooks (
SessionStart,Stop,PreToolUse,PostToolUse,Notification, etc.) machine-wide for anyone who runs the standalone CLI from a source build, with no visible error beyond a log line that's easy to miss between two lines both saying "installed".Suggested fix
(i.e. make the parameter always mean "directory containing the hook script", removing the ambiguity between the two call sites.)
Environment
main