From 75ce4ad44fce9df59eb888e8b024a05def7cf74d Mon Sep 17 00:00:00 2001 From: Andre Padez Date: Tue, 28 Jul 2026 12:02:50 +0100 Subject: [PATCH] resolve the claude cli instead of hardcoding /usr/local/bin/claude MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit the path was pinned to /usr/local/bin/claude for the bwrap-sandboxed architecture: the jail ro-bound /usr and saw nothing else, so the installer's real target (~/.local/bin/claude) had to be symlinked somewhere the sandbox could reach. that sandbox is gone, and the constant outlived it — the sidecar could not run anywhere the symlink was absent. a stock macos host has no /usr/local/bin at all. resolve at module load instead: CLAUDE_BIN pins it explicitly, else whatever is on PATH, else the locations the installer writes to. same shape as OPENCODE_BIN in the opencode sidecar. the resolved path is logged at startup. Co-Authored-By: Claude Opus 5 (1M context) --- src/servers/sidecar/claude/claude-manager.ts | 26 +++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/servers/sidecar/claude/claude-manager.ts b/src/servers/sidecar/claude/claude-manager.ts index 155c2dfa..232b17b6 100644 --- a/src/servers/sidecar/claude/claude-manager.ts +++ b/src/servers/sidecar/claude/claude-manager.ts @@ -1,3 +1,5 @@ +import { existsSync } from 'node:fs'; +import { homedir } from 'node:os'; import { join } from 'node:path'; import { query, type Query } from '@anthropic-ai/claude-agent-sdk'; import type { ChatEvent } from '../../api/chat/types'; @@ -7,9 +9,27 @@ import { createParseState, processMessage } from './stream-parser'; const SEND_TIMEOUT_MS = 30 * 60 * 1000; -// Use /usr/local/bin/claude so it's visible inside bwrap sandbox (which ro-binds /usr). -// The actual binary lives at ~/.local/bin/claude, symlinked from /usr/local/bin/claude. -const CLAUDE_BIN = '/usr/local/bin/claude'; +// Where the Claude Code CLI lives. This was hardcoded to /usr/local/bin/claude, which dated from the +// bwrap-sandboxed architecture: the jail ro-bound /usr and saw nothing else, so the installer's real +// target (~/.local/bin/claude) had to be symlinked into a path the sandbox could reach. That sandbox +// is gone, and the hardcoded path made the sidecar unrunnable anywhere it does not exist — a stock +// macOS host has no /usr/local/bin at all. +// +// Resolution order mirrors OPENCODE_BIN in the opencode sidecar: an explicit pin, then PATH, then the +// locations Anthropic's installer actually writes to. +function resolveClaudeBin(): string { + const pinned = process.env.CLAUDE_BIN; + if (pinned) return pinned; + + const onPath = Bun.which('claude'); + if (onPath) return onPath; + + const candidates = [join(homedir(), '.local', 'bin', 'claude'), '/usr/local/bin/claude', '/opt/homebrew/bin/claude']; + return candidates.find((candidate) => existsSync(candidate)) ?? 'claude'; +} + +const CLAUDE_BIN = resolveClaudeBin(); +console.log(`[claude] CLI resolved to ${CLAUDE_BIN}`); // Capture original HOME before user-instance overrides it const HOST_HOME = process.env.HOME!;