Standalone CLI: copyHookScript() double-joins 'dist', so hook script is never copied but CLI logs false success

Author: srasantosCreated Jul 9, 2026Updated Jul 9, 2026

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

bash
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.

bash
$ 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:

typescript
const distRoot = __dirname;              // already == .../dist  (line 63)
...
copyHookScript(distRoot);                 // lines 110, 138

server/src/providers/hook/claude/claudeHookInstaller.ts:

typescript
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

typescript
// 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;
}
typescript
// cli.ts
const ok = copyHookScript(path.join(distRoot, 'hooks'));
console.log(ok ? '[Pixel Agents] Hooks installed' : '[Pixel Agents] Hooks NOT installed — hook script missing');
typescript
// 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

Source: pixel-agents-hq/pixel-agents