feat: zero-config hook relay via --settings inline curl - #55
Conversation
Server now ensures the autonomos-relay.sh hook script is installed and registered in Claude Code's settings.json on every startup. - installHookRelay() runs at startup, after claude binary validation - Copies hook script from packages/server/hooks/ to ~/.claude/hooks/ - Registers on 10 Claude Code hook events with async: true - Idempotent: skips if already installed and up to date - Updates script if bundled version is newer - Uses ~ paths in settings.json (portable across machines) - Non-fatal: warns on failure, server continues No manual setup needed — just start the server and hooks are ready. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| hooks: [ | ||
| { | ||
| type: "command", | ||
| command: `~/.claude/hooks/${HOOK_FILENAME}`, |
There was a problem hiding this comment.
🟡 Warning
Problem: ~ in the hook command path may not expand in all execution environments.
Why it matters: Claude Code likely runs hook commands via execFile or similar (no shell), which means ~ is treated as a literal character — the hook silently never runs. Since async: true suppresses errors, this failure is invisible. The whole feature would be broken in those environments.
Suggested fix:
// Use the already-resolved HOME constant instead of ~
command: `${HOME}/.claude/hooks/${HOOK_FILENAME}`,This is portable and guaranteed to expand correctly at startup time.
|
|
||
| if (changed) { | ||
| settings.hooks = hooks; | ||
| writeFileSync(SETTINGS_FILE, `${JSON.stringify(settings, null, 2)}\n`); |
There was a problem hiding this comment.
🟡 Warning
Problem: writeFileSync on settings.json is not atomic — a crash mid-write corrupts the file.
Why it matters: If the server process is killed while writing, Claude Code ends up with a partial/invalid settings.json, which likely breaks every Claude Code session on the machine until manually repaired.
Suggested fix:
import { writeFileSync, renameSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
// Write to a temp file, then atomically rename into place
const tmp = join(tmpdir(), `settings-${process.pid}.json`);
writeFileSync(tmp, `${JSON.stringify(settings, null, 2)}\n`);
renameSync(tmp, SETTINGS_FILE);rename is atomic on POSIX — either the old or new file is visible, never a partial write.
nox-0x
left a comment
There was a problem hiding this comment.
Clean, well-structured feature — idempotent design is solid and the graceful error handling is the right call. Two warnings worth fixing before relying on this in prod: (1) the ~ in the hook command path risks silent failure if Claude Code uses execFile rather than a shell; swap for the HOME constant already in scope. (2) the settings.json write should go through a temp-file + rename to avoid corrupting Claude Code config on a hard crash.
Replace the file-based hook relay (autonomos-relay.sh + installHooks.ts) with per-session hook injection via Claude Code's --settings flag. How it works: - createSession() passes --settings with hooks config as inline JSON - Each hook event fires: curl -d @- $AUTONOMOS_SERVER/api/hooks/$SESSION_ID - AUTONOMOS_SERVER and AUTONOMOS_SESSION_ID already injected by buildEnv() - Merges with user's existing hooks (empirically verified) What this eliminates: - No relay script file to install - No global settings.json mutation - No install button needed - No user action required - Sessions outside autonomOS are completely unaffected Deleted: installHooks.ts (no longer needed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| ' -d @- "${AUTONOMOS_SERVER}/api/hooks/${AUTONOMOS_SESSION_ID}"' + | ||
| " >/dev/null 2>&1 &"; | ||
| const hookEntry = { | ||
| matcher: "", |
There was a problem hiding this comment.
🟢 Suggestion
Problem: matcher: "" for PreToolUse/PostToolUse is non-obvious — an empty-string glob typically matches nothing in most pattern libraries, so it's unclear whether this intentionally means "match all tools" or is a quirk of how Claude Code resolves the pattern.
Why it matters: If "" is ever changed to a stricter match (e.g. after a Claude Code update), tool-use hooks silently stop firing and dashboard status icons stop updating for those events. No error, no log.
Suggested fix:
const hookEntry = {
// Empty string matches all tools in CC hook resolution (empirically verified).
// Use "*" if CC ever adds explicit glob support to make the intent clear.
matcher: "",
hooks: [{ type: "command", command: hookCmd, timeout: 3, async: true }],
};Just a one-liner comment to document the intent — makes the next reader (or future-you) confident this is deliberate.
nox-0x
left a comment
There was a problem hiding this comment.
Clean, well-executed refactor — the previous warnings (~ path expansion, writeFileSync atomicity) are fully addressed by switching to inline --settings. The zero-file, zero-config approach is strictly better than the old hook installer. One suggestion left on matcher: "" for PreToolUse/PostToolUse: just add a comment clarifying that empty-string is intentional and empirically verified, so future-you (or a CC update) does not silently break tool-use events. No blocking issues — good to merge.
Summary
Replace the file-based hook relay with per-session hook injection via
--settingsflag. Zero config, zero files, zero user action.How it works
AUTONOMOS_SERVERandAUTONOMOS_SESSION_IDalready injected bybuildEnv()What this eliminates
autonomos-relay.shscript fileinstallHooks.tsmodifying settings.json~/.claude/settings.jsonmutationTest plan
/api/hookscurlfailure is silent (2s timeout, async, >/dev/null)🤖 Generated with Claude Code