Files
LinusandClaude Opus 5.5 0315d5816e Ask for the model provider on every start
run.sh shows a window (zenity form, terminal prompts without zenity) for
a base URL, token, auth header and optional model list. Left empty,
Claude Desktop starts normally with the claude.ai sign-in. Otherwise it
runs in its third-party gateway mode: run.sh writes the managed config
(inferenceProvider "gateway" and friends) and copies it into the created
container as root, because Claude Desktop ignores
/etc/claude-desktop/managed-settings.json unless root owns it. The host
copy is deleted right away; nothing is kept between starts.

Claude Desktop only allows plain http to loopback, so for an http://
gateway launch-apps.sh runs socat on 127.0.0.1:18080 and forwards to it.
localhost is rewritten to host.docker.internal for servers on the host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 18:54:41 +01:00

75 lines
3.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# Container entrypoint: start Claude Desktop and Ghidra, and stop both (so the
# container exits) as soon as either one is closed.
#
# - Ghidra exits when its project window is closed, so its process ending is
# the signal. Closing other Ghidra windows (CodeBrowser, Help) doesn't count.
# - Closing Claude Desktop's window only hides the app (it keeps running for
# the tray icon, which GNOME doesn't show). So its X windows are watched and
# it counts as closed once none of them is open or minimized.
#
# Arguments are passed to Claude Desktop.
set -u
# Job control puts each app in its own process group, so stopping an app also
# stops everything it started (Ghidra's launch script runs java as a child).
set -m
# Plain-HTTP model gateway chosen in run.sh: Claude Desktop only allows http
# to loopback, so it talks to 127.0.0.1:18080 and socat forwards that.
if [[ -n "${CLAUDE_RE_GATEWAY_FORWARD:-}" ]]; then
socat TCP-LISTEN:18080,bind=127.0.0.1,reuseaddr,fork "TCP:${CLAUDE_RE_GATEWAY_FORWARD}" &
fi
claude-desktop "$@" &
claude=$!
"${GHIDRA_INSTALL_DIR:-/opt/ghidra}/support/launch.sh" fg jdk Ghidra "" "" ghidra.GhidraRun &
ghidra=$!
stop_all() {
trap - TERM INT HUP
echo "launch-apps: $1, stopping" >&2
# Claude's main process shuts down its own helpers; killing its whole
# group would take out the GPU process first and trigger restart loops.
# Ghidra's group is needed to reach java behind the launch script.
kill -TERM "$claude" 2>/dev/null
kill -TERM -- "-$ghidra" 2>/dev/null
# Give both a few seconds to shut down cleanly before the container ends.
for _ in {1..10}; do
kill -0 "$claude" 2>/dev/null || kill -0 -- "-$ghidra" 2>/dev/null || break
sleep 0.5
done
exit 0
}
trap 'stop_all "received stop signal"' TERM INT HUP
host="$(hostname)"
# True if any of this container's Claude windows is Normal (shown) or Iconic
# (minimized). A closed window is Withdrawn or loses WM_STATE altogether.
# WM_CLIENT_MACHINE filters out Claude windows from other containers or apps
# on the same X server.
claude_window_open() {
local w props
for w in $(xwininfo -root -tree 2>/dev/null \
| awk '/\("com\.anthropic\.claude" "com\.anthropic\.Claude"\)/ {print $1}'); do
props="$(xprop -id "$w" WM_CLIENT_MACHINE WM_STATE 2>/dev/null)" || continue
[[ "$props" == *"WM_CLIENT_MACHINE(STRING) = \"$host\""* ]] || continue
[[ "$props" =~ "window state: "(Normal|Iconic) ]] && return 0
done
return 1
}
seen=0 # only arm the window check after Claude's window has appeared
misses=0 # require a few consecutive misses to ride out brief remaps
while :; do
sleep 1 & wait $!
kill -0 "$ghidra" 2>/dev/null || stop_all "Ghidra exited"
kill -0 "$claude" 2>/dev/null || stop_all "Claude Desktop exited"
if claude_window_open; then
seen=1 misses=0
elif (( seen )) && (( ++misses >= 3 )); then
stop_all "Claude Desktop window closed"
fi
done