Skip to content

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

Description

@srasantos

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions