2525// For multi-harness dev runs prefer AGENTLOCK_DEV_HOME=./dev which
2626// re-roots every detector AND the daemon's apply paths.
2727
28- import { existsSync } from "node:fs" ;
28+ import { chmodSync , mkdirSync , writeFileSync } from "node:fs" ;
2929import { join , resolve } from "node:path" ;
3030
3131import { detectAll } from "../detect/index.ts" ;
@@ -38,18 +38,58 @@ import {
3838 executeUninstallOps ,
3939 readExistingFiles ,
4040} from "../util/install-fs.ts" ;
41- import { home } from "../util/paths.ts" ;
41+ import { binDir , home , isWin } from "../util/paths.ts" ;
4242import { mintAttestedSession , type AttestedTier } from "../util/session-mint.ts" ;
4343
44- // Source-tree default: cli/agentlock is a bash wrapper that does
45- // `exec bun run cli/src/index.ts "$@"`, so harnesses can spawn
46- // `agentlock hook codex <event>` without needing a compiled binary.
47- // `process.execPath` alone points at `bun`, which crashes (exit 1) when
48- // invoked as `bun hook codex pre-tool-use` because `hook` isn't a script.
49- function defaultAgentlockBinary ( ) : string {
50- const wrapper = resolve ( import . meta. dir , ".." , ".." , "agentlock" ) ;
51- if ( existsSync ( wrapper ) ) return wrapper ;
52- return process . execPath ;
44+ // Stable wrapper path: <agentlockHome>/bin/agentlock. Lives in our state dir,
45+ // not in the package manager's volatile node_modules tree, so package
46+ // upgrades / reinstalls don't strand the wired hook command at a path the
47+ // shell can't spawn (which renders as red "PreToolUse hook error" banners
48+ // in Claude Code, and similar in Cursor / Codex). Re-running `agentlock
49+ // install` rewrites the wrapper, picking up any new index.ts location.
50+ //
51+ // The wrapper itself is bash-only — Windows wiring lands separately.
52+ export function installAndResolveAgentlockBinary ( ) : string {
53+ if ( isWin ( ) ) {
54+ throw new Error (
55+ "agentlock install: Windows wrapper not yet supported. Use macOS/Linux for now." ,
56+ ) ;
57+ }
58+ const indexPath = resolve ( import . meta. dir , ".." , "index.ts" ) ;
59+ const dir = binDir ( ) ;
60+ const wrapper = join ( dir , "agentlock" ) ;
61+ const body = `#!/usr/bin/env bash\nexec bun run "${ indexPath } " "$@"\n` ;
62+ mkdirSync ( dir , { recursive : true } ) ;
63+ writeFileSync ( wrapper , body , { flag : "w" } ) ;
64+ chmodSync ( wrapper , 0o755 ) ;
65+ return wrapper ;
66+ }
67+
68+ // Tiny health-check script wired into Claude Code's `statusLine` config.
69+ // Output renders as a UI element under the chat — never injected into the
70+ // model's input stream — so the user sees live "is the daemon up?" without
71+ // a prompt-injection vector. Curl with a 200ms timeout keeps the status
72+ // line snappy; a hung daemon fails to "offline" instead of stalling the UI.
73+ export function installStatusLineScript ( ) : string {
74+ if ( isWin ( ) ) {
75+ throw new Error (
76+ "agentlock install: Windows status-line not yet supported. Use macOS/Linux for now." ,
77+ ) ;
78+ }
79+ const dir = binDir ( ) ;
80+ const script = join ( dir , "agentlock-status" ) ;
81+ const body = `#!/usr/bin/env bash
82+ url="\${AGENTLOCK_DAEMON_URL:-http://127.0.0.1:7878}"
83+ if curl --max-time 1 -fs "$url/v1/health" >/dev/null 2>&1; then
84+ printf 'OpenAgentLock \\xe2\\x9c\\x93'
85+ else
86+ printf 'OpenAgentLock \\xe2\\x9a\\xa0 daemon offline'
87+ fi
88+ ` ;
89+ mkdirSync ( dir , { recursive : true } ) ;
90+ writeFileSync ( script , body , { flag : "w" } ) ;
91+ chmodSync ( script , 0o755 ) ;
92+ return script ;
5393}
5494
5595type InstallTier = "unattested" | AttestedTier ;
@@ -352,16 +392,22 @@ export async function runInstall(argv: string[] = []): Promise<void> {
352392 cursorHooks ,
353393 ] ) ;
354394
395+ // Write the status-line script alongside the binary wrapper. Daemon
396+ // wires this path into ~/.claude/settings.json `statusLine` so users
397+ // see live OAL health without any chat injection.
398+ const statusLineScript = installStatusLineScript ( ) ;
399+
355400 const planReq = {
356401 session_id : sessionId ,
357402 harnesses : chosen ,
358403 daemon_url : daemonUrl ,
359404 config_dir_override : flags . configDirOverride ,
360405 // Pass an absolute path so Codex's command-hook spawn doesn't depend
361- // on PATH at hook-fire time. Source-tree dev runs use the
362- // `cli/agentlock` wrapper; AGENTLOCK_BINARY lets release builds
363- // override (e.g. point at the compiled single-file binary).
364- agentlock_binary : process . env . AGENTLOCK_BINARY ?? defaultAgentlockBinary ( ) ,
406+ // on PATH at hook-fire time. The wrapper lives under agentlockHome()
407+ // so it survives package-manager upgrades; AGENTLOCK_BINARY lets
408+ // release builds override (e.g. point at a compiled single-file binary).
409+ agentlock_binary : process . env . AGENTLOCK_BINARY ?? installAndResolveAgentlockBinary ( ) ,
410+ status_line_script : statusLineScript ,
365411 harness_config_dirs : hostConfigDirs ,
366412 existing_files : existingFiles ,
367413 } ;
0 commit comments