diff --git a/claude_desktop/CHANGELOG.md b/claude_desktop/CHANGELOG.md index 747ecfa636..04ba951d2e 100644 --- a/claude_desktop/CHANGELOG.md +++ b/claude_desktop/CHANGELOG.md @@ -1,3 +1,14 @@ +## 1.13 (10-07-2026) + +- Add the official Claude Code stable package, `tmux`, `ripgrep`, and a pinned upstream `ttyd` binary for both supported architectures. +- Add an optional authenticated Claude Code web terminal on port `7681`; reconnecting clients attach to the same persistent tmux session. +- Validate and canonicalize `terminal_workspace`, restrict it to supported storage roots, and never re-own existing directories. +- Order the terminal after service initialization and start tmux with an explicit Bash shell and workspace. +- Add `claude-headroom` for Headroom's supported wrapper, reusing the supervised proxy with `--no-proxy` while leaving RTK management to the add-on. +- Keep Claude Desktop on the MCP-only Headroom integration and remove the ineffective `ANTHROPIC_BASE_URL` export from its launch environment. +- Preserve ownership of Claude settings for the configured runtime user and remove only the exact add-on-managed RTK hook when RTK is disabled. +- Document the shared-home architecture, separate Desktop/CLI sessions, direct HTTP terminal security, shared concurrent sessions, and configurable data location. + ## 1.12 (09-07-2026) - Minor bugs fixed ## 1.11 (09-07-2026) diff --git a/claude_desktop/Dockerfile b/claude_desktop/Dockerfile index 9ddb9fa01b..873fffe11a 100644 --- a/claude_desktop/Dockerfile +++ b/claude_desktop/Dockerfile @@ -42,26 +42,48 @@ RUN chmod 744 /ha_lsio.sh && if grep -qr "lsio" /etc; then /ha_lsio.sh "$CONFIGL # Add rootfs COPY rootfs/ / -RUN find . -type f \( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \; +RUN find . -type f \( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \; && \ + chmod +x /usr/local/bin/claude-direct /usr/local/bin/claude-headroom /usr/local/bin/claude-terminal-shell # Uses /bin for compatibility purposes # hadolint ignore=DL4005 RUN if [ ! -f /bin/sh ] && [ -f /usr/bin/sh ]; then ln -s /usr/bin/sh /bin/sh; fi && \ if [ ! -f /bin/bash ] && [ -f /usr/bin/bash ]; then ln -s /usr/bin/bash /bin/bash; fi -# Install Claude Desktop and Python tooling -RUN curl -fsSLo /usr/share/keyrings/claude-desktop-archive-keyring.asc https://downloads.claude.ai/claude-desktop/key.asc && \ +# Install Claude Desktop, Claude Code, browser-terminal tooling, and Python tooling +RUN install -d -m 0755 /etc/apt/keyrings && \ + curl -fsSLo /usr/share/keyrings/claude-desktop-archive-keyring.asc https://downloads.claude.ai/claude-desktop/key.asc && \ + curl -fsSLo /etc/apt/keyrings/claude-code.asc https://downloads.claude.ai/keys/claude-code.asc && \ echo "deb [arch=amd64,arm64 signed-by=/usr/share/keyrings/claude-desktop-archive-keyring.asc] https://downloads.claude.ai/claude-desktop/apt/stable stable main" > /etc/apt/sources.list.d/claude-desktop.list && \ + echo "deb [signed-by=/etc/apt/keyrings/claude-code.asc] https://downloads.claude.ai/claude-code/apt/stable stable main" > /etc/apt/sources.list.d/claude-code.list && \ apt-get update && \ apt-get install -y --no-install-recommends \ claude-desktop \ + claude-code \ python3-pip \ libsecret-1-0 \ dbus-x11 \ - git gh && \ + git \ + gh \ + ripgrep \ + tmux && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* +# ttyd is not packaged in Debian bookworm. Install the pinned upstream static binary. +ARG TTYD_VERSION="1.7.7" +RUN set -eux; \ + case "$(dpkg --print-architecture)" in \ + amd64) ttyd_arch="x86_64" ;; \ + arm64) ttyd_arch="aarch64" ;; \ + *) echo "Unsupported architecture for ttyd: $(dpkg --print-architecture)" >&2; exit 1 ;; \ + esac; \ + curl -fsSL --retry 3 --retry-delay 2 \ + -o /usr/local/bin/ttyd \ + "https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}/ttyd.${ttyd_arch}"; \ + chmod 0755 /usr/local/bin/ttyd; \ + /usr/local/bin/ttyd --version + # Install token-optimization tool prerequisites and binaries at build time RUN apt-get update && \ apt-get install -y --no-install-recommends nodejs && \ diff --git a/claude_desktop/README.md b/claude_desktop/README.md index 6dd6289446..69d75b8ebe 100644 --- a/claude_desktop/README.md +++ b/claude_desktop/README.md @@ -4,7 +4,8 @@ ![Supports amd64 Architecture][amd64-shield] ![Project Maintenance][maintenance-shield] -Run the Claude Desktop Linux app inside a LinuxServer.io Selkies container and stream it through Home Assistant ingress. +Run Claude Desktop and an optional persistent Claude Code web terminal in one +LinuxServer.io Selkies add-on. ## Installation @@ -13,60 +14,177 @@ Run the Claude Desktop Linux app inside a LinuxServer.io Selkies container and s 3. Start the add-on and open the web UI from the sidebar. 4. Sign in with your Claude account from the Desktop app. -Claude Desktop sign-in requires a claude.ai plan that supports the Desktop app. API keys are not accepted by the Desktop application. Anthropic's Linux beta does not include Computer Use or dictation. +Claude Desktop sign-in requires a claude.ai plan that supports the Desktop app. +API keys are not accepted by the Desktop application. Anthropic's Linux beta +currently does not include Computer Use or dictation. + +## Architecture + +Claude Desktop and Claude Code run as separate clients inside the same add-on. +They share the configured persistent home directory, Git credentials, +repositories, Claude Code configuration, Headroom storage, and RTK +configuration, but they do not share or hand off a conversation. + +- **Claude Desktop** uses Headroom through its MCP tools. +- **Claude Code** uses Headroom's supported `headroom wrap claude` integration. +- **RTK** filters Claude Code Bash output through its `PreToolUse` hook. +- **tmux** keeps the terminal session running when the browser disconnects. ## Features - Claude Desktop in single-app Selkies mode. -- Home Assistant ingress support. -- Persistent `$HOME` under `/data/data`, preserving Claude Desktop and Claude Code sign-in state across restarts. +- Home Assistant ingress support for Claude Desktop. +- Official Claude Code stable package installed in the same image. +- Optional authenticated `ttyd` web terminal on port `7681`. +- Persistent `tmux` session shared by reconnecting terminal clients. +- Persistent `$HOME` at the configured `data_location` (default `/data/data`), + preserving Desktop and Claude Code state across restarts. - Optional runtime Claude Desktop updates from Anthropic's apt repository. - Optional extra apt and pip package installation. -- Baked-in `git` and GitHub CLI (`gh`) with optional startup credential configuration. -- Custom script support through the repository standard `claude_desktop.sh` script. -- Optional bundled Claude Code optimization tools: headroom, rtk, and caveman. -- Headroom dashboard exposed on mapped port `8787` when `install_headroom` is enabled. -- Low-power defaults: GPU device mapping, `AUTO_GPU=1`, `SELKIES_FRAMERATE=30`, `/tmp` tmpfs, and `$HOME/.cache` redirected to `/tmp/cache`. +- Baked-in `git`, GitHub CLI (`gh`), `ripgrep`, and terminal tooling. +- Custom script support through the repository standard `claude_desktop.sh`. +- Optional bundled Claude Code optimization tools: Headroom, RTK, and Caveman. +- Headroom dashboard exposed on mapped port `8787` when enabled. +- Low-power defaults for GPU mapping, Selkies frame rate, and volatile caches. + +## Claude Code terminal setup + +The terminal service is enabled in the add-on configuration but remains +unavailable until authentication is configured. Port `7681` is not mapped by +default. + +1. Set a unique `terminal_password`. The existing `PASSWORD` option is accepted + only as a compatibility fallback. +2. Optionally set `terminal_username` and `terminal_workspace`. +3. Map container port `7681` to a host port in the add-on **Network** section. +4. Restart the add-on. +5. Reach `http://:7681` only through an encrypted VPN or an + HTTPS reverse proxy, then sign in with the configured terminal credentials. + +The terminal opens in a persistent tmux session. Closing the browser detaches +from tmux rather than terminating commands that are already running. + +Start the optimized Claude Code path with: + +```shell +claude-headroom +``` + +This reuses the supervised Headroom proxy on `127.0.0.1:8787` and launches +Claude Code with the required routing. Headroom is told not to install RTK +because the add-on already maintains the RTK hook in +`~/.claude/settings.json`. + +To bypass Headroom for troubleshooting, run: + +```shell +claude-direct +``` + +Running `claude` directly is equivalent to the direct path. The first Claude +Code launch may require its own account authentication; Desktop and Claude Code +store separate client credentials even though both use the configured +persistent home directory. + +### Multiple concurrent clients + +Every browser connection attaches to the same tmux session. Concurrent clients +therefore see the same terminal, keystrokes, and resize events. This is useful +for reconnecting to one long-running session, but it is not an isolated +multi-user terminal. + +### Terminal user and permissions + +The service drops privileges to the LinuxServer `abc` account before starting +ttyd. The effective numeric UID and GID follow the configured `PUID` and `PGID`. +Using `PUID: 0` can provide root-equivalent access inside the add-on; use a +non-zero UID/GID where your storage permissions allow it. + +The configured workspace must resolve to the persistent home directory or a +subdirectory of `/share`, `/media`, `/mnt`, `/data`, or `/config`. Existing +directories are never re-owned by the terminal service and must already be +readable, writable, and searchable by `abc`. + +### Terminal security + +The direct ttyd endpoint uses HTTP Basic Authentication without TLS. +Credentials and terminal traffic are unencrypted on the network. ttyd also +receives its Basic Authentication credential as a process argument, so it is +visible to processes with sufficient access inside the container. + +Do not expose port `7681` directly to the public internet. Use a VPN such as +WireGuard or Tailscale, or place the endpoint behind an HTTPS reverse proxy. +Use a unique `terminal_password` rather than reusing the Selkies `PASSWORD`. ## Options | Option | Default | Description | | ------ | ------- | ----------- | -| `PUID` / `PGID` | `0` / `0` | User and group used for persistent data ownership. | -| `TZ` | | Optional timezone, for example `America/New_York`. | +| `PUID` / `PGID` | `0` / `0` | Numeric user and group applied by the LinuxServer initialization. | +| `TZ` | | Optional timezone, for example `Europe/Brussels`. | | `KEYBOARD` | | Optional Selkies keyboard layout. | -| `PASSWORD` | | Optional password for direct Selkies ports. Set this before exposing ports `3000` or `3001`. | +| `PASSWORD` | | Optional password for direct Selkies ports and compatibility fallback for terminal authentication. | | `DRINODE` | | Optional GPU device override for Selkies. | | `DNS_server` | `8.8.8.8` | DNS server used by the standard DNS module. | -| `auto_update` | `true` | Check Anthropic's apt repository and upgrade `claude-desktop` at add-on startup. | -| `install_headroom` | `true` | Register the baked-in `headroom` MCP server in Claude Desktop, start the Headroom proxy backend, and expose the Headroom dashboard on mapped port `8787` at `/dashboard` when that port is open. This exposes the `headroom_compress`/`headroom_retrieve`/`headroom_stats` context-compression tools inside the app. (Claude Desktop overrides `ANTHROPIC_BASE_URL`, so transparent proxy compression is not possible — MCP is the supported path; see [headroom #869](https://github.com/headroomlabs-ai/headroom/issues/869).) Disabling removes the MCP entry and stops the backend/dashboard service. | -| `install_rtk` | `true` | Configure the rtk Claude Code `PreToolUse` hook in the persistent Claude Code settings. | -| `install_caveman` | `true` | Install the caveman Claude Code plugin into the persistent Claude Code home. | -| `install_github_cli` | `true` | Enable first-start checks and setup for the baked-in `git` and `gh` commands. | -| `github_token` | | Optional GitHub personal access token used to authenticate `gh` and configure Git credentials for GitHub. | +| `auto_update` | `true` | Upgrade `claude-desktop` from Anthropic's apt repository at startup. | +| `enable_terminal` | `true` | Enable the supervised Claude Code web-terminal service. | +| `terminal_username` | `claude` | Username used by ttyd Basic Authentication. | +| `terminal_password` | | Dedicated terminal password. The service idles when this and `PASSWORD` are empty. | +| `terminal_workspace` | | Initial directory; defaults to `/workspace`. | +| `install_headroom` | `true` | Enable Headroom MCP for Desktop and the supervised local proxy reused by `claude-headroom`. | +| `install_rtk` | `true` | Configure RTK's Claude Code `PreToolUse` hook. | +| `install_caveman` | `true` | Install the Caveman Claude Code plugin in the persistent Claude home. | +| `install_github_cli` | `true` | Enable setup checks for the baked-in `git` and `gh` commands. | +| `github_token` | | Optional GitHub token used to authenticate `gh` and Git operations. | | `github_username` | | Optional global Git author name. | | `github_email` | | Optional global Git author email. | | `ha_smart_context` | `true` | Enable Home Assistant smart context support for Claude tooling. | | `enable_ha_mcp` | `true` | Enable Home Assistant MCP support for Claude tooling. | -| `dangerously_skip_permissions` | `false` | Expose Claude Code's dangerous permission skip mode option. Only enable if you understand the risk. | -| `additional_apps` | | Comma-separated Debian apt packages to install at startup, for example `htop,git`. | -| `additional_pip` | | Comma-separated pip packages to install at startup. Installs use `--break-system-packages`. | -| `data_location` | `/data/data` | Persistent home directory location. Keep this persistent so Claude sign-in survives restarts. | -| `networkdisks`, `cifsusername`, `cifspassword`, `cifsdomain` | | Standard SMB mount options. | -| `localdisks` | | Standard local disk mount option. | -| `env_vars` | `[]` | Extra environment variables to export into the container. This can override `SELKIES_*` defaults. | +| `dangerously_skip_permissions` | `false` | Reserved compatibility option; it is not applied by the terminal launcher. | +| `additional_apps` | | Comma-separated Debian apt packages to install at startup. | +| `additional_pip` | | Comma-separated pip packages installed with `--break-system-packages`. | +| `data_location` | `/data/data` | Persistent home directory for both Claude clients and tooling. | +| `env_vars` | `[]` | Additional environment variables exported inside the container. | + +## Headroom behavior + +When `install_headroom` is enabled, the add-on registers `headroom mcp serve` in +Claude Desktop and starts a supervised local Headroom backend. Desktop can use +`headroom_compress`, `headroom_retrieve`, and `headroom_stats` through MCP. + +Claude Desktop overrides `ANTHROPIC_BASE_URL`, so it is deliberately launched +without proxy injection. The web terminal instead provides `claude-headroom`, +which reuses the supervised proxy through Headroom's `--no-proxy` mode. RTK +setup remains owned by the add-on through Headroom's `--no-rtk` mode. + +The Headroom dashboard remains available at: + +```text +http://:8787/dashboard +``` + +when the `8787/tcp` port is mapped. Treat this endpoint as sensitive and do not +expose it directly to the public internet. ## Custom scripts -The add-on includes the repository standard custom-script executor. On first start, it seeds a `claude_desktop.sh` file in the add-on config directory from the shared template. Commands in that script run during startup, allowing local customization without rebuilding the image. +The add-on includes the repository standard custom-script executor. On first +start, it seeds `claude_desktop.sh` in the add-on config directory. Commands in +that script run during startup, allowing local customization without rebuilding +the image. ## Data and cache locations -Persistent state is stored in the configured `data_location`. Claude Desktop stores sign-in data below `~/.config/Claude`, and Claude Code/tool configuration is stored below `~/.claude`. Volatile cache data is redirected to `/tmp/cache` through `$XDG_CACHE_HOME` and `$HOME/.cache`. +Persistent state is stored in the configured `data_location` (default +`/data/data`): -## Headroom dashboard +- Claude Desktop sign-in: `~/.config/Claude` +- Claude Code settings, hooks, sessions, and plugins: `~/.claude` +- Default terminal workspace: `~/workspace` +- Headroom and RTK user state: their standard paths below the shared home -When `install_headroom` is enabled, the add-on starts the local Headroom proxy backend and maps port `8787` by default. Open `http://:8787/dashboard` to view the Headroom live savings dashboard. If you disable the `8787/tcp` port mapping, the backend binds to localhost for MCP use only and the dashboard is not externally exposed. +Volatile cache data is redirected to `/tmp/cache` through `$XDG_CACHE_HOME` and +`$HOME/.cache`. [aarch64-shield]: https://img.shields.io/badge/aarch64-yes-green.svg [amd64-shield]: https://img.shields.io/badge/amd64-yes-green.svg diff --git a/claude_desktop/config.yaml b/claude_desktop/config.yaml index cc74a60335..f545094a26 100644 --- a/claude_desktop/config.yaml +++ b/claude_desktop/config.yaml @@ -2,7 +2,7 @@ arch: - aarch64 - amd64 audio: true -description: Claude Desktop streamed through a browser with LinuxServer Selkies +description: Claude Desktop and a persistent Claude Code terminal streamed from one add-on devices: - /dev/dri - /dev/dri/card0 @@ -44,6 +44,10 @@ options: ha_smart_context: true enable_ha_mcp: true dangerously_skip_permissions: false + enable_terminal: true + terminal_username: claude + terminal_password: "" + terminal_workspace: "" github_token: "" github_username: "" install_caveman: true @@ -54,9 +58,11 @@ panel_admin: false panel_icon: mdi:robot-happy ports: 3001/tcp: null + 7681/tcp: null 8787/tcp: 8787 ports_description: - 3001/tcp: Web interface https + 3001/tcp: Claude Desktop web interface + 7681/tcp: HTTP Basic-auth Claude Code terminal (no TLS) 8787/tcp: Headroom dashboard and proxy privileged: - SYS_ADMIN @@ -80,6 +86,10 @@ schema: ha_smart_context: bool? enable_ha_mcp: bool? dangerously_skip_permissions: bool? + enable_terminal: bool? + terminal_username: match(^[A-Za-z0-9_.-]+$)? + terminal_password: password? + terminal_workspace: str? github_token: password? github_username: str? install_caveman: bool @@ -90,5 +100,5 @@ slug: claude_desktop tmpfs: true udev: true url: https://github.com/alexbelgium/hassio-addons -version: "1.12" +version: "1.13" video: true diff --git a/claude_desktop/rootfs/defaults/autostart b/claude_desktop/rootfs/defaults/autostart index b3d68ce167..45f4fd0399 100644 --- a/claude_desktop/rootfs/defaults/autostart +++ b/claude_desktop/rootfs/defaults/autostart @@ -20,12 +20,13 @@ if [ -s "$CLAUDE_DESKTOP_COMMAND_FILE" ]; then else CLAUDE_DESKTOP_COMMAND="$DEFAULT_CLAUDE_DESKTOP_COMMAND" fi -if bashio::config.true 'install_headroom'; then - export ANTHROPIC_BASE_URL="http://127.0.0.1:8787" -fi + +# Headroom is intentionally not injected into the Desktop process. Claude Desktop overrides +# ANTHROPIC_BASE_URL, so Desktop uses the registered Headroom MCP tools instead. The Claude Code +# terminal uses the supported `headroom wrap claude` integration through claude-headroom. # Launch the configured command. If a custom/wrapped command fails to start, fall back to -# the plain Claude Desktop launch so the desktop app still comes up for the user. +# the plain Claude Desktop launch so the app always comes up for the user. if ! sh -c "$CLAUDE_DESKTOP_COMMAND"; then if [ "$CLAUDE_DESKTOP_COMMAND" != "$DEFAULT_CLAUDE_DESKTOP_COMMAND" ]; then echo "autostart: '$CLAUDE_DESKTOP_COMMAND' failed; falling back to default Claude Desktop launch" >&2 diff --git a/claude_desktop/rootfs/etc/cont-init.d/82-claude_tools.sh b/claude_desktop/rootfs/etc/cont-init.d/82-claude_tools.sh index 71bbdbe6d3..9484a20c04 100755 --- a/claude_desktop/rootfs/etc/cont-init.d/82-claude_tools.sh +++ b/claude_desktop/rootfs/etc/cont-init.d/82-claude_tools.sh @@ -2,6 +2,9 @@ # shellcheck shell=bash set -e set -o pipefail + +PUID="$(if bashio::config.has_value 'PUID'; then bashio::config 'PUID'; else echo '0'; fi)" +PGID="$(if bashio::config.has_value 'PGID'; then bashio::config 'PGID'; else echo '0'; fi)" mkdir -p "$HOME/.claude" CLAUDE_DESKTOP_COMMAND_FILE="/tmp/claude-desktop-command" @@ -138,24 +141,66 @@ PY else bashio::log.warning "rtk is not available" fi -elif [ -f "$HOME/.claude/settings.json" ] && grep -q 'rtk' "$HOME/.claude/settings.json"; then - bashio::log.info "Removing rtk Claude Code hook" +elif [ -f "$HOME/.claude/settings.json" ]; then + bashio::log.info "Removing the add-on-managed rtk Claude Code hook" python3 - <<'PY' || bashio::log.warning "Unable to remove rtk hook automatically" import json from pathlib import Path + path = Path.home() / ".claude" / "settings.json" data = json.loads(path.read_text()) -hooks = data.get("hooks", {}) -for event, entries in list(hooks.items()): - if isinstance(entries, list): - hooks[event] = [entry for entry in entries if "rtk" not in json.dumps(entry)] - if not hooks[event]: - hooks.pop(event, None) -if hooks: - data["hooks"] = hooks -else: - data.pop("hooks", None) -path.write_text(json.dumps(data, indent=2) + "\n") +if not isinstance(data, dict): + raise TypeError("Claude settings must contain a JSON object") + +hooks = data.get("hooks") +if not isinstance(hooks, dict): + raise SystemExit(0) + +entries = hooks.get("PreToolUse") +if not isinstance(entries, list): + raise SystemExit(0) + +changed = False +filtered_entries = [] +for entry in entries: + if not isinstance(entry, dict) or entry.get("matcher") != "Bash": + filtered_entries.append(entry) + continue + + commands = entry.get("hooks") + if not isinstance(commands, list): + filtered_entries.append(entry) + continue + + filtered_commands = [ + command + for command in commands + if not ( + isinstance(command, dict) + and command.get("type") == "command" + and command.get("command") == "rtk hook claude" + ) + ] + if len(filtered_commands) == len(commands): + filtered_entries.append(entry) + continue + + changed = True + if filtered_commands: + updated_entry = dict(entry) + updated_entry["hooks"] = filtered_commands + filtered_entries.append(updated_entry) + +if changed: + if filtered_entries: + hooks["PreToolUse"] = filtered_entries + else: + hooks.pop("PreToolUse", None) + if hooks: + data["hooks"] = hooks + else: + data.pop("hooks", None) + path.write_text(json.dumps(data, indent=2) + "\n") PY fi @@ -170,3 +215,11 @@ else bashio::log.info "Disabling caveman Claude Code plugin" find "$HOME/.claude" -maxdepth 4 -iname '*caveman*' -exec rm -rf {} + 2> /dev/null || true fi + +# Startup configuration runs as root, while Claude Desktop and the web terminal run as abc. +# Return managed persistent files to the configured runtime UID/GID after all writes complete. +for managed_path in "$HOME/.claude" "$HOME/.config/Claude"; do + if [ -e "$managed_path" ]; then + chown -R -- "${PUID}:${PGID}" "$managed_path" || bashio::log.warning "Unable to set ownership on $managed_path" + fi +done diff --git a/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-claude-terminal/dependencies.d/init-services b/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-claude-terminal/dependencies.d/init-services new file mode 100644 index 0000000000..e69de29bb2 diff --git a/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-claude-terminal/run b/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-claude-terminal/run new file mode 100644 index 0000000000..afd56cb69a --- /dev/null +++ b/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-claude-terminal/run @@ -0,0 +1,101 @@ +#!/usr/bin/with-contenv bashio +# shellcheck shell=bash +set -e +set -o pipefail + +declare port=7681 +declare username +declare password="" +declare workspace +declare canonical_workspace + +export PATH="${HOME:-/data/data}/.local/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}" + +if bashio::config.has_value 'enable_terminal' && ! bashio::config.true 'enable_terminal'; then + bashio::log.info "svc-claude-terminal: terminal disabled; idling" + exec sleep infinity +fi + +if [ -z "${HOME:-}" ]; then + bashio::log.error "svc-claude-terminal: HOME is not initialized; idling" + exec sleep infinity +fi + +if ! command -v ttyd >/dev/null 2>&1 || ! command -v tmux >/dev/null 2>&1 || ! command -v claude >/dev/null 2>&1; then + bashio::log.error "svc-claude-terminal: ttyd, tmux, or Claude Code is missing; idling" + exec sleep infinity +fi + +username="claude" +if bashio::config.has_value 'terminal_username'; then + username="$(bashio::config 'terminal_username')" +fi + +if bashio::config.has_value 'terminal_password'; then + password="$(bashio::config 'terminal_password')" +elif bashio::config.has_value 'PASSWORD'; then + bashio::log.warning "svc-claude-terminal: using PASSWORD as fallback for terminal authentication; prefer a unique terminal_password" + password="$(bashio::config 'PASSWORD')" +fi + +if [ -z "$password" ]; then + bashio::log.warning "svc-claude-terminal: set terminal_password (or PASSWORD) before mapping port ${port}; terminal will remain disabled" + exec sleep infinity +fi + +workspace="${HOME}/workspace" +if bashio::config.has_value 'terminal_workspace'; then + workspace="$(bashio::config 'terminal_workspace')" +fi + +if [[ "$workspace" != /* ]]; then + bashio::log.error "svc-claude-terminal: terminal_workspace must be an absolute path; idling" + exec sleep infinity +fi + +if [ -L "$workspace" ]; then + bashio::log.error "svc-claude-terminal: terminal_workspace must not be a symbolic link; idling" + exec sleep infinity +fi + +if ! canonical_workspace="$(realpath -m -- "$workspace")"; then + bashio::log.error "svc-claude-terminal: unable to resolve terminal_workspace '$workspace'; idling" + exec sleep infinity +fi +workspace="$canonical_workspace" + +case "$workspace" in + "$HOME" | "$HOME"/* | /share/* | /media/* | /mnt/* | /data/* | /config/*) + ;; + *) + bashio::log.error "svc-claude-terminal: terminal_workspace must be the configured data_location or a subdirectory of /share, /media, /mnt, /data, or /config; idling" + exec sleep infinity + ;; +esac + +if [ ! -e "$workspace" ]; then + if ! install -d -m 0750 -o abc -g abc -- "$workspace"; then + bashio::log.error "svc-claude-terminal: failed to create workspace '$workspace'; idling" + exec sleep infinity + fi +elif [ ! -d "$workspace" ]; then + bashio::log.error "svc-claude-terminal: terminal_workspace '$workspace' is not a directory; idling" + exec sleep infinity +fi + +if ! s6-setuidgid abc test -r "$workspace" || + ! s6-setuidgid abc test -w "$workspace" || + ! s6-setuidgid abc test -x "$workspace"; then + bashio::log.error "svc-claude-terminal: workspace '$workspace' must be readable, writable, and searchable by user abc; idling" + exec sleep infinity +fi + +export CLAUDE_TERMINAL_WORKSPACE="$workspace" + +bashio::log.info "svc-claude-terminal: starting authenticated ttyd terminal on port ${port}; workspace=${workspace}" +exec s6-setuidgid abc ttyd \ + -p "$port" \ + -W \ + -O \ + -c "${username}:${password}" \ + /usr/local/bin/claude-terminal-shell diff --git a/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-claude-terminal/type b/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-claude-terminal/type new file mode 100644 index 0000000000..5883cff0cd --- /dev/null +++ b/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-claude-terminal/type @@ -0,0 +1 @@ +longrun diff --git a/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-headroom/run b/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-headroom/run index 941b6768d6..fa912f0dbd 100755 --- a/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-headroom/run +++ b/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/svc-headroom/run @@ -1,16 +1,11 @@ #!/usr/bin/with-contenv bashio -# Headroom optimization proxy — runs ONLY as a local backend for the headroom MCP tools -# (headroom_compress / headroom_retrieve, registered in 82-claude_tools.sh). It is NOT a -# traffic router: no client's ANTHROPIC_BASE_URL is pointed at it, so the Claude Desktop -# Electron app (which force-overrides ANTHROPIC_BASE_URL — headroom #869) and every other -# client are unaffected. Without this backend the MCP tools cannot store/retrieve compressed -# content and no savings are ever recorded. +# Headroom optimization proxy — local backend for Claude Desktop MCP and Claude Code. declare port=8787 declare host=127.0.0.1 if bashio::config.true 'install_headroom' && command -v headroom >/dev/null 2>&1; then - bashio::log.info "svc-headroom: starting headroom proxy on ${host}:${port}; dashboard available at /dashboard when the port is mapped (MCP backend only; no client routing)" - exec headroom proxy --host "${host}" --port "${port}" --code-aware + bashio::log.info "svc-headroom: starting local Headroom proxy on ${host}:${port}" + exec s6-setuidgid abc headroom proxy --host "${host}" --port "${port}" --code-aware fi bashio::log.info "svc-headroom: install_headroom disabled or headroom not found; idling" diff --git a/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/svc-claude-terminal b/claude_desktop/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/svc-claude-terminal new file mode 100644 index 0000000000..e69de29bb2 diff --git a/claude_desktop/rootfs/usr/local/bin/claude-direct b/claude_desktop/rootfs/usr/local/bin/claude-direct new file mode 100644 index 0000000000..9b16e20c3f --- /dev/null +++ b/claude_desktop/rootfs/usr/local/bin/claude-direct @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +exec claude "$@" diff --git a/claude_desktop/rootfs/usr/local/bin/claude-headroom b/claude_desktop/rootfs/usr/local/bin/claude-headroom new file mode 100644 index 0000000000..4c8fa247dd --- /dev/null +++ b/claude_desktop/rootfs/usr/local/bin/claude-headroom @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +if ! command -v claude >/dev/null 2>&1; then + echo "Claude Code is not installed or is not on PATH." >&2 + exit 127 +fi + +if ! command -v headroom >/dev/null 2>&1; then + echo "Headroom is unavailable; start Claude Code directly with claude-direct." >&2 + exit 127 +fi + +if ! curl -fsS --max-time 3 "http://127.0.0.1:8787/readyz" >/dev/null; then + echo "The supervised Headroom proxy is not ready on 127.0.0.1:8787. Ensure install_headroom is enabled and check the add-on log." >&2 + exit 1 +fi + +# Reuse the s6-supervised proxy instead of starting a competing proxy. RTK is already managed +# through the persistent Claude Code PreToolUse hook, so Headroom must not reinstall it. +exec headroom wrap claude --port 8787 --no-proxy --no-rtk -- "$@" diff --git a/claude_desktop/rootfs/usr/local/bin/claude-terminal-shell b/claude_desktop/rootfs/usr/local/bin/claude-terminal-shell new file mode 100644 index 0000000000..daf449397e --- /dev/null +++ b/claude_desktop/rootfs/usr/local/bin/claude-terminal-shell @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ -z "${HOME:-}" ]; then + echo "Claude terminal: HOME is not initialized." >&2 + exit 1 +fi + +export SHELL="/bin/bash" +export PATH="${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}" + +workspace="${CLAUDE_TERMINAL_WORKSPACE:-${HOME}/workspace}" +session_name="${CLAUDE_TMUX_SESSION:-claude}" + +if [ ! -d "$workspace" ]; then + echo "Claude terminal: workspace does not exist: $workspace" >&2 + exit 1 +fi + +cd -- "$workspace" + +# Reattach every browser connection to the same terminal session. Closing the browser detaches +# the client but leaves Claude Code and other commands running inside tmux. +exec tmux new-session -A -s "$session_name" -c "$workspace"