Skip to content

setup run from $HOME writes relative hook commands into Claude Code's user-global settings.json — hooks then fail outside $HOME (loudly in interactive sessions, silently in -p) #52

@MoonCaves

Description

@MoonCaves

Summary

Project-local is mnemon's documented default — per the README: "mnemon setup defaults to local (project-scoped .claude/), recommended for most users." This report is not about that default; it's about one degenerate case where it misfires:

When mnemon setup --target claude-code runs with cwd == $HOME, the "project-local" ./.claude/ is ~/.claude/ — Claude Code's user-global config directory. The relative hook commands setup writes (.claude/hooks/mnemon/prime.sh, ...) land in the user-global settings.json, which loads for every session on the machine — but the relative paths only resolve when a session's working directory happens to be $HOME. From any other directory, interactive sessions log

Stop hook error: Failed with non-blocking status code: /bin/sh: 1: .claude/hooks/mnemon/stop.sh: not found

on every turn, headless (-p) sessions fail silently, and the memory integration quietly stops working (no priming, no recall, no remember).

Why cwd==$HOME is common rather than exotic: installs are increasingly agent-mediated — a user points a Claude Code session at the README and the agent runs the command from wherever it sits. Non-interactive remote installs land there 100% of the time: ssh host 'mnemon setup --target claude-code --yes' always runs with cwd=$HOME.

Reproduction

Version-stamped: mnemon 0.1.13, Claude Code CLI 2.1.163, reproduced on macOS (arm64) and Debian Linux (x86_64).

cd ~ && mnemon setup --target claude-code --yes   # documented default (project-local)
cd /tmp && claude                                  # any session outside $HOME
# interactive session → "stop.sh: not found" every turn (observed in production; see Evidence)
# claude -p from the same dir → no error shown; hooks simply never run

Evidence (all observed, with controls)

Write-side (mnemon 0.1.13, binary invoked directly — no agent in the loop):

setup invoked from lands in command form outcome
$HOME (default scope) ~/.claude/settings.json (user-global) relative broken outside $HOME
genuine project dir (default scope) <proj>/.claude/settings.json relative works (see loader note)
anywhere with --global ~/.claude/settings.json absolute works everywhere (control)

Loader-side (Claude Code 2.1.163, headless -p, sandboxed HOME):

  • Project settings.json loads only when the session cwd is the project dir (no parent/git-root fallback observed) — exactly where the relative path resolves. So genuine project-local installs are unaffected; the relative form is even the portable choice there (survives repo clones, unlike an absolute path).
  • The user-global file loads for every session regardless of cwd; a relative command there resolved only from $HOME. An absolute command in the same file ran from every cwd tested (control).
  • Stated as version-observed behavior, not a contract.

Production instance (before/after): on a server where an agent had run the README install from /root: three relative entries in /root/.claude/settings.json, every interactive session logging the not-found errors (transcripts retained). Re-running mnemon setup --target claude-code --yes --global repaired in place — relative entries replaced (not duplicated), hooks verified firing from /tmp afterward.

Frequency: on a personal fleet of 8 machines (a 9th is client-managed and not used with Claude — excluded), 6 had mnemon installed. 2 of those 6 had landed in this broken form, both since repaired — one is the production before/after above.

Proposed minimal fix (PR attached)

A collision guard in ClaudeRegisterHooks: if the project-local config dir resolves to the same directory as Claude Code's user-global config dir (symlink-resolved comparison on both sides; honors CLAUDE_CONFIG_DIR), write absolute hook commands for exactly that case and print a note suggesting --global for explicitness. This honors the user-global file's existing contract (your --global path already writes absolute commands) without touching the project-local default — genuine project-local installs keep their current relative form, byte-for-byte.

A stricter alternative if you prefer it: hard-error on the collision ("this is ~/.claude; use --global"). Most explicit, and agents handle instructive errors well — but it makes --yes automation exit nonzero. The attached PR implements reroute+note; happy to flip it to the error form, it's a small change and both behaviors are covered by tests.

Related observations (no PR, separate from the bug)

  • Setup run in a subdirectory of a project places .claude/ where (per the loader behavior above) sessions started at the project root never load it — a silent no-op install. A one-line end-of-setup note ("installed to <dir>/.claude; loads for sessions started in <dir>") would surface both this and the $HOME case without changing any behavior. Happy to follow up if useful.
  • More generally: agent-mediated installs run from arbitrary working directories (the fleet numbers above). If you'd like the non-interactive path to behave differently in light of that, glad to implement whatever direction you choose — no change proposed here; the documented project-local default is respected as-is.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions