#!/bin/sh
#
# claude-code-native-update.sh — installs/updates Claude Code to the latest
# version, running natively on FreeBSD, without Linuxulator and without
# depending on npm (which, starting from version 2.1.2xx, no longer ships
# the JavaScript bundle).
#
# How it works: Anthropic builds the official Linux binary using
# `bun build --compile`, which embeds the JavaScript bundle directly into
# the ELF executable using Bun's internal format (a module delimited by
# markers such as "/$bunfs/root/<path>\0...\0// @bun @bytecode @bun-cjs\n(function(...)").
# This script downloads that binary, extracts the cli.js module using
# those markers (without relying on manually guessed offsets—the same
# markers are used internally by Bun, so they remain valid even if the
# bundle size changes between releases), and executes it with a native
# FreeBSD `bun` already installed on the system (built from
# /usr/ports/lang/bun).
# Usage:
# ./claude-code-native-update.sh # installs/updates to the latest version
# ./claude-code-native-update.sh 2.1.209 # installs a specific version
#
# Re-runnable: if the installed version already matches the requested
# version, nothing is downloaded again. Designed to be safely invoked
# from cron/periodic as well, so it can keep itself automatically updated.
set -eu
PREFIX="${CLAUDE_NATIVE_PREFIX:-/opt/claude-code-native}"
BIN_LINK="${CLAUDE_NATIVE_BIN:-/usr/local/bin/claude}"
BUN_BIN="${CLAUDE_NATIVE_BUN:-$(command -v bun || true)}"
RELEASES_BASE="https://downloads.claude.ai/claude-code-releases"
log() { echo "==> $*"; }
err() { echo "error: $*" >&2; exit 1; }
[ -n "$BUN_BIN" ] || err "bun not found in PATH. Install it first: cd /usr/ports/lang/bun && make install (or 'pkg install bun' if it is already in the repository), or set CLAUDE_NATIVE_BUN=/path/to/bun"
[ -x "$BUN_BIN" ] || err "bun is not executable: $BUN_BIN"
case "$(uname -m)" in
amd64|x86_64) PLATFORM="linux-x64" ;;
aarch64|arm64) PLATFORM="linux-arm64" ;;
*) err "unsupported architecture: $(uname -m)" ;;
esac
VERSION="${1:-}"
if [ -z "$VERSION" ]; then
log "checking latest available version..."
VERSION="$(fetch -q -o - "${RELEASES_BASE}/latest")" \
|| err "unable to read ${RELEASES_BASE}/latest"
fi
[ -n "$VERSION" ] || err "empty version, something went wrong while fetching"
log "target version: $VERSION"
VERDIR="${PREFIX}/${VERSION}"
CURRENT_VERSION=""
[ -f "${PREFIX}/current/VERSION" ] && CURRENT_VERSION="$(cat "${PREFIX}/current/VERSION")"
if [ "$CURRENT_VERSION" = "$VERSION" ] && [ -f "${PREFIX}/current/claude-entry.js" ]; then
log "already at version $VERSION, nothing to do"
exit 0
fi
WORK="$(mktemp -d "${TMPDIR:-/tmp}/claude-native.XXXXXX")"
trap 'rm -rf "$WORK"' EXIT
BINURL="${RELEASES_BASE}/${VERSION}/${PLATFORM}/claude"
log "downloading official binary: $BINURL"
fetch -o "${WORK}/claude-bin" "$BINURL" || err "download failed"
log "extracting the cli.js module from the Bun bundle (parsing /\$bunfs/root/ markers)"
python3 - "$WORK/claude-bin" "$WORK/claude-cli.js" <<'PYEOF'
import re, sys
src_path, out_path = sys.argv[1], sys.argv[2]
data = open(src_path, "rb").read()
# Every module compiled by Bun is preceded by a header similar to:
# /$bunfs/root/<path>\x00[/$bunfs/root/<path>\x00]// @bun @bytecode @bun-cjs\n(function(exports, require, module, __filename, __dirname) { ... }
# The exact bytes preceding the path may vary (it is not always a plain \x00), so
# we anchor on the fixed string "// @bun @bytecode @bun-cjs\n(function(...)"
# and simply verify that "cli.js" appears shortly before it, without
# assuming an identical null-byte layout for every module.
ANCHOR = b"// @bun @bytecode @bun-cjs\n(function(exports, require, module, __filename, __dirname) {"
positions = [mo.start() for mo in re.finditer(re.escape(ANCHOR), data)]
if not positions:
sys.exit("no Bun modules found in the binary (@bun @bytecode marker missing) - format changed?")
target = None
for p in positions:
preceding = data[max(0, p - 300):p]
if b"cli.js" in preceding:
target = p
break
if target is None:
sys.exit("Bun modules found, but none with 'cli.js' in the path shortly before it - format changed?")
start = target + len(b"// @bun @bytecode @bun-cjs\n") # beginning of "(function(exports, ..."
if data[start:start + 10] != b"(function(":
sys.exit("the content after the marker does not start with the expected CJS wrapper")
# End of module = header of the next module ("\x00/$bunfs/root/...",
# the same structure seen before the current anchor), or end of file
# if cli.js is the last module in the bundle.
next_header = data.find(b"\x00/$bunfs/root/", start)
end = next_header if next_header != -1 else len(data)
module_bytes = data[start:end]
if not module_bytes.rstrip().endswith(b")"):
sys.exit("the extracted module does not end as expected (truncated bundle?)")
with open(out_path, "wb") as f:
f.write(module_bytes)
print(f"extracted {len(module_bytes)} bytes")
PYEOF
[ -s "${WORK}/claude-cli.js" ] || err "extraction failed, cli.js is empty"
cat > "${WORK}/claude-entry.js" <<'EOF'
const fs = require('fs');
const path = require('path');
const code = fs.readFileSync(path.join(__dirname, 'claude-cli.js'), 'utf8');
const fn = eval(code);
fn(exports, require, module, __filename, __dirname);
EOF
log "verifying that the extracted bundle actually works (claude --version)"
GOTVER="$("$BUN_BIN" "${WORK}/claude-entry.js" --version 2>&1 | head -1)" \
|| err "the extracted bundle does not start with bun: $GOTVER"
case "$GOTVER" in
${VERSION}*) ;;
*) err "unexpected version after extraction: '$GOTVER' (expected $VERSION)" ;;
esac
log "OK: $GOTVER"
log "installing into ${VERDIR}"
if [ "$(id -u)" != 0 ] && [ ! -w "$(dirname "$PREFIX")" ]; then
SUDO="sudo"
else
SUDO=""
fi
$SUDO mkdir -p "$VERDIR"
$SUDO cp "${WORK}/claude-cli.js" "${WORK}/claude-entry.js" "${WORK}/claude-bin" "$VERDIR/"
$SUDO chmod +x "${VERDIR}/claude-bin"
echo "$VERSION" | $SUDO tee "${VERDIR}/VERSION" > /dev/null
$SUDO ln -sfn "$VERDIR" "${PREFIX}/current"
$SUDO tee "$BIN_LINK" > /dev/null <<EOF
#!/bin/sh
export USE_BUILTIN_RIPGREP=0
export DISABLE_AUTOUPDATER=1
exec "$BUN_BIN" "${PREFIX}/current/claude-entry.js" "\$@"
EOF
$SUDO chmod +x "$BIN_LINK"
log "done: $BIN_LINK -> Claude Code $VERSION (native FreeBSD, bun, no Linuxulator)"