Compare commits

..

1 Commits

Author SHA1 Message Date
claude[bot]
042e3bf05a fix(webtop_kde): tolerate init-video/init-selkies-config failures to stop crash loop
The Selkies desktop init oneshots do best-effort device/permission setup
(mknod /dev/input/*, chmod /tmp/selkies*, /dev/dri perms) that is only
partially permitted in the HA add-on sandbox. A non-zero exit from a
oneshot crash-loops the container along with svc-selkies. Apply the same
tolerant-oneshot fix already used in claude_desktop's 20-folders.sh.

Closes #2918

Co-authored-by: Alexandre <44178713+alexbelgium@users.noreply.github.com>
2026-07-28 07:00:14 +00:00
36 changed files with 399 additions and 847 deletions

View File

@@ -1,45 +0,0 @@
#!/usr/bin/env bash
# Replace every symlink in the checked-out repository with a real copy of its target.
#
# Add-ons share files by symlinking across add-on directories (e.g. webtop/rootfs ->
# ../webtop_kde/rootfs, and files inside it -> ../../../../claude_desktop/rootfs/...). A
# Docker build context is a single add-on directory, so any symlink that escapes it has to be
# materialised before the build.
#
# The loop repeats because resolving one symlink can create others: copying a directory
# symlink with `cp -a` preserves the symlinks *inside* it, and those copies are not part of
# the file list the current pass is iterating over. Repeating until a pass finds nothing makes
# the result independent of the order `find` happens to return.
set -euo pipefail
for _ in 1 2 3 4 5; do
mapfile -t links < <(find . -type l)
if [ "${#links[@]}" -eq 0 ]; then
exit 0
fi
for link in "${links[@]}"; do
target=$(readlink -f "$link" || true)
if [ -z "$target" ] || [ ! -e "$target" ]; then
# Fail rather than drop it. A broken link here means an add-on is missing a file
# it expects to ship; silently removing it produces an image that builds fine and
# misbehaves at runtime, which is far harder to diagnose than a red build.
echo "::error::Broken symlink: $link -> $(readlink "$link")"
exit 1
fi
rm "$link"
if [ -d "$target" ]; then
mkdir -p "$link"
cp -a "$target/." "$link/"
else
cp "$target" "$link"
fi
done
done
if [ -n "$(find . -type l)" ]; then
echo "::error::Symlinks still present after 5 resolution passes; possible symlink cycle"
find . -type l
exit 1
fi

View File

@@ -108,9 +108,6 @@ jobs:
- name: ↩️ Checkout
uses: actions/checkout@v7.0.1
- name: Resolve symlinks in repository copy
run: bash .github/scripts/resolve_symlinks.sh
- name: Copy templates into addon build context
env:
ADDON: ${{ matrix.addon }}

View File

@@ -142,7 +142,24 @@ jobs:
persist-credentials: false
- name: Resolve symlinks in repository copy
run: bash .github/scripts/resolve_symlinks.sh
run: |
set -euo pipefail
find . -type l | while read -r link; do
target=$(readlink -f "$link" || true)
if [ -z "$target" ]; then
echo "Skipping broken symlink: $link"
continue
fi
rm "$link"
if [ -d "$target" ]; then
mkdir -p "$link"
cp -a "$target/." "$link/"
else
cp "$target" "$link"
fi
done
- name: Copy templates into addon build context
env:

View File

@@ -56,7 +56,7 @@ If you want to do add the repository manually, please follow the procedure highl
### Number of addons
- In the repository : 137
- In the repository : 136
- Installed : 591099
### Top 3
@@ -796,14 +796,6 @@ If you want to do add the repository manually, please follow the procedure highl
![amd64][amd64-badge]
![full_access][full_access-badge]
&#10003; ![image](https://api.iconify.design/mdi/docker.svg) [Portainer Business Edition](portainer_be/) : Manage your Docker environment with ease (Business Edition)
&emsp;&emsp;![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fportainer_be%2Fconfig.yaml)
![Update](https://img.shields.io/badge/dynamic/json?label=Updated&query=%24.last_update&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fportainer_be%2Fupdater.json)
![aarch64][aarch64-badge]
![amd64][amd64-badge]
![ingress][ingress-badge]
&#10003; [Postgres 15](postgres_15/) : Postgres 15 with VectorChord support
&emsp;&emsp;![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fpostgres_15%2Fconfig.yaml)

View File

@@ -1,20 +1,3 @@
## 1.37 (28-07-2026)
- Fix the recurring "For your security, sign in again to keep using Claude." prompt for real. The v1.35 fix was ineffective: `--password-store=basic` was reaching the process (confirmed on a live install's `/proc/<pid>/cmdline`), yet the app kept logging `safeStorage not available, tokens will not persist` and `Encryption not available, returning empty env vars` on every launch. The missing half is an application-side opt-in — Electron refuses its built-in `basic_text` backend unless the app calls `safeStorage.setUsePlainTextEncryption(true)` before `ready`, and Claude Desktop never calls it (the symbol is present in the Electron binary but absent from `resources/app.asar`). So `isEncryptionAvailable()` stayed `false` and the auth token was never persisted, exactly as when the keyring backend was forced without a keyring daemon. Verified against a standalone Electron of the same generation: with `--password-store=basic` and no opt-in `isEncryptionAvailable()` is `false`; with the opt-in it is `true`, and a *separate later process* decrypts a blob written by an earlier one — which is the restart survival this add-on needs.
- **The patch (`86-claude_safestorage.sh` + `claude-safestorage-patch.js`, new).** A small Node script injects that one opt-in call into the app's main bundle (`.vite/build/index.pre.js`) inside `app.asar`. Insertion is placed *after* the bundle's leading `"use strict";` rather than before it, because a directive prologue only takes effect as the first statement — prepending ahead of it would silently drop the entire main process out of strict mode. The archive is rebuilt rather than edited in place, since an asar's header stores a byte offset and length per file: entries flagged `unpacked` (which live in `app.asar.unpacked/`, not in the archive body) and symlink entries are carried through untouched, so the native-module layout is preserved, and the per-file SHA-256 `integrity` record is recomputed for the one file that changed. The injector fails closed: anything that does not open with an unambiguously terminated `"use strict"` directive is refused rather than patched by guesswork, since `"use strict" + x` is an expression and injecting into it would yield a syntax error. The rebuild is written to a temporary file, `fsync`ed, and then fully re-validated from disk — entry count, every offset and length parsed as an exact in-bounds integer, the last body ending precisely at EOF, and the patched entry's recomputed SHA-256 — before it is renamed over the original and the directory synced; the temporary file is removed on every failure path. So a torn, truncated or short write can never replace a working archive. Verified byte-for-byte against the real archive: same 211 entries, no `unpacked`/symlink flag drift, no trailing slack, every integrity hash valid, and content differing in exactly one file.
- **Why it re-runs every boot.** `81-claude_update.sh` apt-upgrades `claude-desktop` on startup, and a new package ships a fresh, unpatched `app.asar`; a one-shot patch at image build time would therefore be undone by the first update. The script is marker-guarded, so a boot where the app did not change is a no-op, and it is numbered `86-` to land after both the update check and `85-openbox_autostart.sh`, while still completing before any s6 service — i.e. before the desktop launches the app. A failure is logged and swallowed rather than propagated, and the patcher is run under a 120s `timeout`: an unpatched app still runs, it just forgets the sign-in, which is not worth blocking startup over. The hook ends in an explicit `exit 0` so no logging branch can turn a non-fatal patch failure into a failed cont-init, and it sweeps stale temporary archives itself — `timeout` kills the patcher outright, so the run that hits the cap cannot execute its own cleanup, and each stranded file is archive-sized.
- **`gnome-keyring` stays out of the image.** Re-adding it would reintroduce the first-boot keyring password prompt that blocks Claude Desktop from launching at all. This route needs no keyring, no D-Bus Secret Service, no daemon and no password, so no prompt can appear. The trade-off is unchanged from v1.35 and is inherent to the `basic` backend: its key is fixed rather than gated by a keyring, so any process that can read the persistent `$HOME/.config/Claude` profile can recover the stored credentials.
- **One-time step after upgrading.** The previously stored session is already stale, so a single sign-in is still needed once after this update; it then persists across restarts.
## 1.36.4 (28-07-2026)
- Fix Selkies dying with a Rust `RuntimeDirNotSet` unwrap panic just after `Data WebSocket Server listening on port 8081`, and the data websocket then being proxied to the wrong port. Upstream relies on s6-rc ordering: `init-selkies-config` publishes `XDG_RUNTIME_DIR` and `CUSTOM_WS_PORT` into the s6 envdir and `svc-selkies` starts afterwards. The add-on entrypoint replaces s6-overlay and starts every `s6-rc.d` run script in parallel with no dependency graph, so Selkies can snapshot the envdir before that oneshot has written to it -- which is why it bound port 8081 (its own default) instead of the 8082 nginx proxies to, and why its Wayland compositor found no runtime directory to bind a socket in. `20-folders.sh` now exports both variables inside each run script, where no start ordering can lose them, and corrects the base image's `$HOME/.XDG` override where that write happens instead of appending a correction after the `exit 0` that the oneshot-tolerance block adds -- which meant the correction never ran on any boot after the first.
## 1.36.3 (28-07-2026)
- Make the Selkies startup scripts add-on agnostic so `webtop` and `webtop_kde` can share them by symlink instead of carrying their own drifted copies. `20-folders.sh` now derives its default data location from the home directory the Dockerfile baked into the `abc` user (`getent passwd abc`) rather than hardcoding `/data/data`, and the `permission_mode: bypass` root guard is skipped on add-ons that do not declare that option. `80-configuration.sh` falls back to `pip` when the image does not ship `uv`. No behaviour change for Claude Desktop: `getent passwd abc` returns `/data/data`, which is exactly the value that was hardcoded before.
## 1.36.2 (28-07-2026)
- Minor bugs fixed

View File

@@ -105,10 +105,7 @@ RUN if [ ! -f /bin/sh ] && [ -f /usr/bin/sh ]; then ln -s /usr/bin/sh /bin/sh; f
# Install Claude Desktop, Claude Code, Python tooling, and lightweight local validators.
# gnome-keyring is intentionally NOT installed: it prompts for a keyring password on first
# boot, which blocks Claude Desktop from launching. Sign-in persistence instead uses Electron's
# built-in --password-store=basic (rootfs/defaults/autostart) — no daemon, no prompt — together
# with rootfs/etc/cont-init.d/86-claude_safestorage.sh, which patches app.asar to opt into that
# backend. Electron ignores --password-store=basic without the app-side opt-in, so both are
# required for the sign-in to survive a restart.
# built-in --password-store=basic (rootfs/defaults/autostart) — no daemon, no prompt.
# The cowork virtualization stack (qemu-system-x86 + ovmf firmware) lets Claude Code launch
# its sandbox microVM; libseccomp2 and libcap-ng0 are the shared libraries the source-built
# virtiofsd daemon links against at runtime. Docker itself is NOT installed here: this base

View File

@@ -9,12 +9,11 @@ streamed desktop.
v1.24-ish removed `gnome-keyring` again because it prompts for a keyring password on first
boot, which blocked Claude Desktop from ever launching — but the launch flag was left
forcing the now-daemonless libsecret backend, so `safeStorage` silently went unavailable
again (recurring "sign in again", and the Claude app's dispatch tab showing the desktop as
offline until a fresh sign-in was done from a computer). v1.35 switched to
`--password-store=basic` plus a cont-init script that re-syncs the persistent openbox
`autostart` from the image on every boot — **but that flag alone does nothing**, and the bug
survived it untouched. Actually fixed in v1.37, which adds the application-side opt-in the
`basic` backend requires; see "Why v1.35 did not work" below.
again (recurring "sign in again", and — new in this round — the Claude app's dispatch tab
showing the desktop as offline until a fresh sign-in was done from a computer). Fixed in
v1.35: switched to `--password-store=basic` (Electron's built-in store, no keyring
involved at all) plus a cont-init script that re-syncs the persistent openbox `autostart`
from the image on every boot, so the fix reaches existing installs, not just fresh ones.
- **Planned only:** Problem A (in-desktop browser for OAuth) is intentionally not implemented.
The image ships no browser; complete the login with the user-side workaround below.
@@ -103,60 +102,14 @@ Re-adding gnome-keyring would just reintroduce the original launch-blocking prom
a dead end without also solving *that* — hence the shipped fix below avoids keyring
entirely.
### Why v1.35 did not work
v1.35 assumed `--password-store=basic` makes `isEncryptionAvailable()` "always true". It does
not. On a live install the flag was confirmed on the running process:
```
$ tr '\0' ' ' < /proc/2247/cmdline
/usr/lib/claude-desktop/claude-desktop --no-sandbox --disable-dev-shm-usage --password-store=basic
```
and the app still logged, on every launch:
```
[warn] safeStorage not available, tokens will not persist
[warn] Encryption not available, returning empty env vars
[error] Electron safeStorage encryption is not available on this system, cannot store allowlist cache
```
Electron deliberately refuses its own `basic_text` backend unless the **application** opts in
by calling `safeStorage.setUsePlainTextEncryption(true)` before the `ready` event. The symbol
is present in the shipped Electron binary but absent from `resources/app.asar` — Claude Desktop
never calls it. So v1.35 replaced one unavailable backend with another.
Confirmed against a standalone Electron of the same generation, all with
`--password-store=basic`:
| case | `isEncryptionAvailable()` |
| --- | --- |
| no opt-in (= Claude Desktop as shipped) | `false` |
| `setUsePlainTextEncryption(true)` | `true`, `encryptString` works |
| fresh process, decrypting the earlier process's blob | `true`, plaintext recovered |
The third row is the one that matters: it is the restart survival this add-on needs.
### Fix (shipped, v1.37)
1. `claude_desktop/rootfs/defaults/autostart` launches with `--password-store=basic` instead of
`gnome-libsecret` — Electron's built-in fixed-key store: no daemon, no prompt. Secrets land
under `$HOME/.config/Claude`, and `HOME=/data/data` is persistent add-on storage, so the
session survives restarts. `basic` trades away OS-backed at-rest protection: unlike
### Fix (shipped, v1.35)
1. `claude_desktop/rootfs/defaults/autostart` launches with
`--password-store=basic` instead of `gnome-libsecret`. `basic` is Electron's built-in
fixed-key store: `isEncryptionAvailable()` is always `true`, no daemon, no prompt. Secrets
land under `$HOME/.config/Claude`, and `HOME=/data/data` is persistent add-on storage, so
the session survives restarts. `basic` trades away OS-backed at-rest protection: unlike
`gnome-libsecret`, its encryption key isn't gated by a keyring daemon, so any process able
to read the persistent `$HOME/.config/Claude` profile can recover the saved credentials.
1b. `claude_desktop/rootfs/etc/cont-init.d/86-claude_safestorage.sh` +
`claude_desktop/rootfs/usr/local/bin/claude-safestorage-patch.js` (new) supply the opt-in
the flag depends on, by injecting `safeStorage.setUsePlainTextEncryption(true)` into the
app's main bundle inside `app.asar`. The injection goes *after* the bundle's leading
`"use strict";` — a directive prologue only counts as the first statement, so prepending
ahead of it would drop the main process out of strict mode. The archive is rebuilt (asar
headers store per-file offsets, so content cannot simply grow in place), preserving
`unpacked` and symlink entries and recomputing the per-file SHA-256 `integrity` record for
the single changed file; the result is verified and only then renamed over the original.
It re-runs every boot after `81-claude_update.sh`, because an apt upgrade of
`claude-desktop` ships a fresh unpatched `app.asar`; it is marker-guarded, so an unchanged
app is a no-op, and a failure is logged rather than propagated (an unpatched app still
runs, it just forgets the sign-in).
2. A passwordless keyring was considered instead (keeps libsecret encryption-at-rest without
a prompt) and rejected: the keyring DB would live in the same persistent volume as the
ciphertext it's "protecting," so it adds ~no real confidentiality in this single-user
@@ -180,12 +133,9 @@ normally and dispatch stays online regardless of which device connects first aft
## Files this plan touched
- `claude_desktop/rootfs/defaults/autostart` — drop the keyring bootstrap; launch with
`--password-store=basic`.
- `claude_desktop/rootfs/etc/cont-init.d/85-openbox_autostart.sh` — new in v1.35; syncs the
persistent autostart from the image on every boot.
- `claude_desktop/rootfs/etc/cont-init.d/86-claude_safestorage.sh` and
`claude_desktop/rootfs/usr/local/bin/claude-safestorage-patch.js` — new in v1.37; the
app-side `safeStorage` opt-in that makes `--password-store=basic` actually take effect.
- `claude_desktop/rootfs/etc/cont-init.d/85-openbox_autostart.sh` — new; syncs the persistent
autostart from the image on every boot.
- `claude_desktop/Dockerfile` — corrected stale comment (gnome-keyring is not installed).
- `claude_desktop/CHANGELOG.md` / `config.yaml` — v1.35, then v1.37.
- `claude_desktop/CHANGELOG.md` / `config.yaml` — v1.35.
Problem A (in-desktop browser for OAuth) remains planned-only; not touched by this change.

View File

@@ -122,5 +122,5 @@ slug: claude_desktop
tmpfs: true
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "1.37"
version: "1.36.2"
video: true

View File

@@ -8,15 +8,9 @@
# surfaced as recurring "sign in again" prompts and (because the stale session also fails the
# elevated-access OAuth check) the Claude app's dispatch tab showing this desktop as offline.
#
# --password-store=basic uses Electron's built-in fixed-key store instead: no daemon and no
# prompt. Secrets land under $HOME/.config/Claude, and HOME=/data/data is persistent add-on
# storage, so the saved sign-in survives restarts.
#
# This flag is only half of it. Electron refuses the basic backend unless the application opts
# in via safeStorage.setUsePlainTextEncryption(true), and Claude Desktop never calls it — with
# the flag alone, isEncryptionAvailable() stays false and the sign-in is still lost on every
# restart. /etc/cont-init.d/86-claude_safestorage.sh injects that opt-in into app.asar before
# this runs; do not drop one without the other.
# --password-store=basic uses Electron's built-in fixed-key store instead: no daemon, no
# prompt, and isEncryptionAvailable() is always true. Secrets land under $HOME/.config/Claude,
# and HOME=/data/data is persistent add-on storage, so the saved sign-in survives restarts.
# Headroom is intentionally not injected into the Desktop process: Claude Desktop
# force-overrides ANTHROPIC_BASE_URL (headroom #869), so Desktop uses the registered Headroom
# MCP tools instead.

View File

@@ -3,33 +3,6 @@
# shellcheck disable=SC2046
set -e
# Shared by every Selkies-based add-on in this repo (claude_desktop, webtop, webtop_kde) via a
# symlink; keep it add-on agnostic. The only per-add-on input is the home directory baked into
# the image by the Dockerfile's `usermod --home <dir> abc`, read back below.
# Default data location for this image: whatever the Dockerfile set as abc's home.
#
# Cached in a marker file rather than read from /etc/passwd on every boot, because this script
# rewrites that entry further down to the *selected* location. On a restart that reuses the
# container's writable layer, re-reading /etc/passwd would hand back the previous selection as
# the "image default", so clearing data_location would strand the user on their old custom path
# instead of restoring the built-in one. The marker shares its lifetime with the /etc/passwd
# edit it compensates for: both live in the writable layer, so a rebuilt or recreated container
# starts from a pristine /etc/passwd and regenerates the marker correctly.
#
# The `|| true` is load-bearing: getent exits 2 when the user does not exist, and under bashio's
# `set -o pipefail` plus this script's `set -e` that aborts the script at the assignment, before
# the fallback below can run. Same trap documented in 21-gpu_permissions.sh.
DEFAULT_LOCATION_MARKER="/etc/.addon_image_home"
if [ ! -s "$DEFAULT_LOCATION_MARKER" ]; then
getent passwd abc 2> /dev/null | cut -d: -f6 > "$DEFAULT_LOCATION_MARKER" || true
fi
DEFAULT_LOCATION="$(cat "$DEFAULT_LOCATION_MARKER" 2> /dev/null || true)"
if [[ -z "$DEFAULT_LOCATION" || "$DEFAULT_LOCATION" == "/" ]]; then
DEFAULT_LOCATION="/config/data"
bashio::log.warning "Could not read the abc home directory from /etc/passwd; defaulting to $DEFAULT_LOCATION"
fi
# Align the shared desktop user (abc) with the configured PUID/PGID before any storage is
# chowned and before any service or s6-setuidgid call resolves abc. The base image's
# init-adduser applies the same remap, but it runs after cont-init, so doing it here first is
@@ -38,8 +11,8 @@ PUID="$(if bashio::config.has_value 'PUID'; then bashio::config 'PUID'; else ech
PGID="$(if bashio::config.has_value 'PGID'; then bashio::config 'PGID'; else echo '1000'; fi)"
# Claude Code refuses bypass-permissions mode under an effective root UID, so bypass mode
# always needs a non-root desktop user. Add-ons without a permission_mode option skip this.
if bashio::config.has_value 'permission_mode' && [ "$(bashio::config 'permission_mode')" = "bypass" ] && [ "$PUID" -eq 0 ]; then
# always needs a non-root desktop user.
if [ "$(bashio::config 'permission_mode')" = "bypass" ] && [ "$PUID" -eq 0 ]; then
bashio::log.warning "permission_mode: bypass cannot run Claude Code as root; using UID 1000 instead of the configured PUID 0"
PUID=1000
fi
@@ -64,7 +37,7 @@ fi
LOCATION="$(bashio::config 'data_location')"
if [[ "$LOCATION" = "null" || -z "$LOCATION" ]]; then
LOCATION="$DEFAULT_LOCATION"
LOCATION="/data/data"
else
LOCATIONOK=""
for location in "/share" "/config" "/data" "/mnt"; do
@@ -74,7 +47,7 @@ else
done
if [ -z "$LOCATIONOK" ]; then
LOCATION="$DEFAULT_LOCATION"
LOCATION="/data/data"
bashio::log.fatal "Your data_location value can only be set in /share, /config, /data or /mnt. It will be reset to the default location : $LOCATION"
fi
fi
@@ -92,66 +65,18 @@ XDG_RUNTIME_DIR="/run/user/$PUID"
mkdir -p "$XDG_RUNTIME_DIR"
chmod 700 "$XDG_RUNTIME_DIR"
# Must agree with the CWS substitution in 90-ingress.sh: nginx proxies the Selkies data
# websocket to this port, and Selkies only listens on it if CUSTOM_WS_PORT reaches its process.
# Validated here, once, because the value goes on to be interpolated into generated shell and
# into a sed replacement in 90-ingress.sh, both of which take the normalised value back out of
# the envdir written below.
SELKIES_WS_PORT="${CUSTOM_WS_PORT:-8082}"
if ! [[ "$SELKIES_WS_PORT" =~ ^[0-9]+$ ]] || [ "$SELKIES_WS_PORT" -lt 1 ] || [ "$SELKIES_WS_PORT" -gt 65535 ]; then
bashio::log.warning "CUSTOM_WS_PORT '${CUSTOM_WS_PORT:-}' is not a valid port number; using 8082"
SELKIES_WS_PORT=8082
fi
# Upstream relies on s6-rc ordering: init-selkies-config publishes XDG_RUNTIME_DIR and
# CUSTOM_WS_PORT into the s6 envdir, and svc-selkies is started afterwards. The add-on
# entrypoint replaces s6-overlay and launches every s6-rc.d run script in parallel, with no
# dependency graph, so a longrun can snapshot the envdir (with-contenv reads it once, at exec)
# before the oneshot has written to it. Selkies is where that shows: it comes up with
# CUSTOM_WS_PORT unset and binds its data websocket on the 8081 default while nginx proxies
# 8082, and on the PIXELFLUX_WAYLAND images it reaches the compositor with no XDG_RUNTIME_DIR
# and panics with `RuntimeDirNotSet` binding the Wayland socket.
#
# Exporting both inside the run scripts puts them in each process's own environment, where no
# start ordering can lose them, and keeps every service agreeing on one runtime dir -- svc-de
# otherwise waits forever on a Wayland socket under a directory Selkies never used.
# Rewrite the home path baked into the image to the user-chosen one. No-op when data_location
# is left at its default. Runs before the exports below are injected, not after: this is a
# blind textual substitution, so a location *under* the image default (data_location
# /config/data_kde/foo against a /config/data_kde image) would otherwise rewrite the freshly
# injected "export HOME=/config/data_kde/foo" into ".../foo/foo".
if [ "$LOCATION" != "$DEFAULT_LOCATION" ]; then
for folders in /defaults /etc/cont-init.d /etc/services.d /etc/s6-overlay/s6-rc.d; do
if [ -d "$folders" ]; then
find "$folders" -type f -exec sed -i "s|$DEFAULT_LOCATION|$LOCATION|g" {} + &> /dev/null || true
fi
done
fi
# Re-derived on every boot rather than injected once behind a marker, for the same reason the
# ~/.bashrc block below is: the run scripts live in the writable layer and survive a restart, so
# a write-once injection pins whatever PUID and CUSTOM_WS_PORT were in force the first time.
# Raising PUID would leave every service exporting a /run/user/<old-uid> the remapped abc user
# cannot use, and clearing a custom CUSTOM_WS_PORT would leave Selkies on the old port while
# 90-ingress.sh moved nginx back to 8082. Strip whatever a previous boot left -- the marked
# block, or the bare exports earlier versions wrote -- then write the current values. No
# upstream run script in these images sets any of these five, so the bare-line sweep only ever
# removes our own.
ENV_BLOCK_BEGIN="# --- BEGIN ADDON ENV (managed) ---"
ENV_BLOCK_END="# --- END ADDON ENV (managed) ---"
for file in /etc/s6-overlay/s6-rc.d/*/run; do
[ -n "$(sed -n '1{/bash/p};q' "$file")" ] || continue
sed -i "/^${ENV_BLOCK_BEGIN}\$/,/^${ENV_BLOCK_END}\$/d" "$file"
sed -i -E '/^export (HOME|FM_HOME|XDG_CACHE_HOME|XDG_RUNTIME_DIR|CUSTOM_WS_PORT)=/d' "$file"
# Each "1a" lands at line 2 and pushes the previous one down, so this reads bottom-up.
sed -i "1a $ENV_BLOCK_END" "$file"
sed -i "1a export HOME=\"$LOCATION\"" "$file"
sed -i "1a export FM_HOME=\"$LOCATION\"" "$file"
sed -i "1a export XDG_CACHE_HOME=\"/tmp/cache\"" "$file"
sed -i "1a export XDG_RUNTIME_DIR=\"$XDG_RUNTIME_DIR\"" "$file"
sed -i "1a export CUSTOM_WS_PORT=\"$SELKIES_WS_PORT\"" "$file"
sed -i "1a $ENV_BLOCK_BEGIN" "$file"
if [ "$(sed -n '1{/bash/p};q' "$file")" ] && ! grep -q '^export XDG_CACHE_HOME=/tmp/cache$' "$file"; then
sed -i "1a export HOME=$LOCATION" "$file"
sed -i "1a export FM_HOME=$LOCATION" "$file"
sed -i "1a export XDG_CACHE_HOME=/tmp/cache" "$file"
fi
done
for folders in /defaults /etc/cont-init.d /etc/services.d /etc/s6-overlay/s6-rc.d; do
if [ -d "$folders" ]; then
find "$folders" -type f -exec sed -i "s|/data/data|$LOCATION|g" {} + &> /dev/null || true
fi
done
sed -i "s|^\(abc:[^:]*:[^:]*:[^:]*:[^:]*:\)[^:]*|\1$LOCATION|" /etc/passwd
@@ -160,7 +85,6 @@ printf "%s" "$LOCATION" > "$S6_ENVDIR/HOME"
printf "%s" "$LOCATION" > "$S6_ENVDIR/FM_HOME"
printf "%s" "/tmp/cache" > "$S6_ENVDIR/XDG_CACHE_HOME"
printf "%s" "$XDG_RUNTIME_DIR" > "$S6_ENVDIR/XDG_RUNTIME_DIR"
printf "%s" "$SELKIES_WS_PORT" > "$S6_ENVDIR/CUSTOM_WS_PORT"
# Re-derived on every boot rather than gated on a "does it already say $LOCATION" grep: that
# guard only ever recognized the CURRENT $LOCATION, so a user who changed data_location and
# later changed it back left two stale HOME/FM_HOME exports in ~/.bashrc, with the last one
@@ -204,16 +128,13 @@ bashio::log.info "Setting ownership to $PUID:$PGID"
chown -R "${PUID}:${PGID}" "$LOCATION" /tmp/cache "$XDG_RUNTIME_DIR" /data
chmod -R 700 "$LOCATION"
# The base init-selkies-config script overrides XDG_RUNTIME_DIR to $HOME/.XDG, which lands on
# persistent storage and conflicts with the tmpfs runtime dir set above. Correct that write
# where it happens rather than re-asserting the value at the end of the oneshot: the tolerance
# block below appends `exit 0`, so on every boot after the first an appended correction sits
# past it and never runs.
# The base init-selkies-config script overrides XDG_RUNTIME_DIR to $HOME/.XDG, which lands
# on persistent storage and conflicts with the tmpfs runtime dir set above. Re-assert the
# tmpfs value at the end of that oneshot so the app and desktop agree on one valid dir.
SELKIES_CONFIG_RUN="/etc/s6-overlay/s6-rc.d/init-selkies-config/run"
if [ -f "$SELKIES_CONFIG_RUN" ]; then
# Drop the trailing correction earlier versions appended, now applied at the source.
sed -i '/^# XDG_RUNTIME_DIR override reconciled$/,+1d' "$SELKIES_CONFIG_RUN"
sed -i "s|^.*> */run/s6/container_environment/XDG_RUNTIME_DIR *\$|printf '%s' '$XDG_RUNTIME_DIR' > /run/s6/container_environment/XDG_RUNTIME_DIR|" "$SELKIES_CONFIG_RUN"
printf '\n# XDG_RUNTIME_DIR override reconciled\nprintf "%%s" "%s" > /run/s6/container_environment/XDG_RUNTIME_DIR\n' "$XDG_RUNTIME_DIR" >> "$SELKIES_CONFIG_RUN"
fi
# The Selkies desktop init oneshots do best-effort device/permission setup (mknod

View File

@@ -2,11 +2,7 @@
# shellcheck shell=bash
set -e
# Shared by every Selkies-based add-on in this repo (claude_desktop, webtop, webtop_kde) via a
# symlink; keep it add-on agnostic. Every option read here is guarded, so an add-on that does
# not declare a given option simply skips that block.
#
# All three images are Debian/Ubuntu-based, so apt is the only system package manager used.
# The image is Debian-based (apt) and always ships uv, so those are the only installers used.
if bashio::config.has_value 'additional_apps'; then
bashio::log.info "Installing additional apps :"
apt-get update -o Acquire::http::Timeout=10 -o Acquire::https::Timeout=10 &> /dev/null || bashio::log.warning "Unable to update apt package lists"
@@ -17,14 +13,9 @@ if bashio::config.has_value 'additional_apps'; then
fi
if bashio::config.has_value 'additional_pip'; then
if command -v uv &> /dev/null; then
pip_install=(uv pip install --system --break-system-packages)
else
pip_install=(pip install --break-system-packages)
fi
for p in $(bashio::config 'additional_pip' | tr ',' ' '); do
bashio::log.green "... pip: $p"
"${pip_install[@]}" "$p" || bashio::log.fatal "Error: pip package $p failed"
uv pip install --system --break-system-packages "$p" || bashio::log.fatal "Error: pip package $p failed"
done
fi

View File

@@ -1,61 +0,0 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# Claude Desktop is launched with --password-store=basic (see /defaults/autostart) so that no
# system keyring — and therefore no keyring password prompt — is ever needed. Electron does not
# accept that backend unless the application itself calls
# safeStorage.setUsePlainTextEncryption(true), and Claude Desktop never does, so without this
# patch safeStorage.isEncryptionAvailable() stays false: the auth token is never persisted and
# the user is asked to sign in again on every start.
#
# claude-safestorage-patch.js injects that opt-in into the app's main bundle inside app.asar.
# It runs on every boot, and after 81-claude_update.sh, on purpose: an apt upgrade of
# claude-desktop replaces app.asar with a fresh unpatched copy. The patcher is marker-guarded,
# so a boot where the app did not change is a cheap no-op.
#
# A failure here must never block startup — the app still runs fine unpatched, it just forgets
# the sign-in — so the patcher's exit status is reported, not propagated, and it is capped with
# a timeout so a pathological archive cannot stall the boot indefinitely.
ASAR="/usr/lib/claude-desktop/resources/app.asar"
PATCHER="/usr/local/bin/claude-safestorage-patch.js"
if [ ! -f "$ASAR" ]; then
bashio::log.warning "Claude Desktop app.asar not found; skipping the safeStorage patch"
exit 0
fi
if [ ! -f "$PATCHER" ]; then
bashio::log.warning "$PATCHER not found; skipping the safeStorage patch"
exit 0
fi
# The patcher removes its own temporary archive on every failure path, but `timeout` kills it
# outright, so a run that hits the cap leaves one behind. Those are archive-sized, and the live
# archive stays unpatched, so every later boot would retry under a new pid and strand another
# copy until the container runs out of space.
cleanup_tmp() {
find "$(dirname "$ASAR")" -maxdepth 1 -name ".$(basename "$ASAR").addon-tmp.*" -delete 2>/dev/null || true
}
if output=$(timeout 120 node "$PATCHER" "$ASAR" 2>&1); then
bashio::log.info "safeStorage: ${output}"
else
rc=$?
if [ "$rc" -eq 124 ]; then
bashio::log.warning "safeStorage patch timed out after 120s; continuing unpatched."
else
bashio::log.warning "safeStorage patch failed (exit ${rc}); the sign-in will not persist."
fi
while IFS= read -r line; do
if [ -n "$line" ]; then
bashio::log.warning "${line}"
fi
done <<< "${output}"
cleanup_tmp
fi
# Explicitly successful: a failed patch is never fatal, so the status of whatever ran last above
# must not become this script's status and abort the boot.
exit 0

View File

@@ -24,15 +24,7 @@ sed -i '/listen \[::\]/d' "${NGINX_CONFIG}"
# Adapt ports and upstream paths for Home Assistant ingress
sed -i "s|3000|$(bashio::addon.ingress_port)|g" "${NGINX_CONFIG}"
sed -i "s|SUBFOLDER|/|g" "${NGINX_CONFIG}"
# Same value 20-folders.sh exports to the Selkies services; both must move together or nginx
# proxies the data websocket to a port nothing listens on. That script also normalises it into
# the s6 envdir, which this one picks up through with-contenv; the check is repeated so a
# malformed value cannot reach the nginx config if 20-folders.sh did not get that far.
CWS="${CUSTOM_WS_PORT:-8082}"
if ! [[ "$CWS" =~ ^[0-9]+$ ]] || [ "$CWS" -lt 1 ] || [ "$CWS" -gt 65535 ]; then
CWS=8082
fi
sed -i "s|CWS|${CWS}|g" "${NGINX_CONFIG}"
sed -i "s|CWS|8082|g" "${NGINX_CONFIG}"
sed -i "s|REPLACE_HOME|${HOME:-/root}|g" "${NGINX_CONFIG}"
sed -i "s|REPLACE_DOWNLOADS_PATH|${HOME:-/config}|g" "${NGINX_CONFIG}"
sed -i '/proxy_buffering/a proxy_set_header Accept-Encoding "";' "${NGINX_CONFIG}"

View File

@@ -1,284 +0,0 @@
#!/usr/bin/env node
/*
* Enable Electron safeStorage for Claude Desktop without a system keyring.
*
* Claude Desktop persists its auth token with Electron's safeStorage. On Linux that is gated
* on a backend: the libsecret backend needs a running Secret Service (gnome-keyring), which is
* intentionally not installed here because it prompts for a keyring password on first boot and
* blocks the app from launching. The app is therefore launched with --password-store=basic
* (Electron's built-in fixed-key store: no daemon, no prompt).
*
* That alone is not enough. Electron refuses the `basic_text` backend unless the *application*
* explicitly opts in by calling safeStorage.setUsePlainTextEncryption(true) before the app is
* ready, and Claude Desktop never calls it. So isEncryptionAvailable() stays false, the token is
* never persisted, and the user is asked to sign in again on every start. There is no equivalent
* command-line switch, and NODE_OPTIONS=--require is ignored by packaged Electron apps, so the
* opt-in has to be injected into the app's own main bundle.
*
* This script does that inside app.asar. It is idempotent (marker-guarded) and re-applied on
* every boot, because 81-claude_update.sh apt-upgrades claude-desktop and a new package ships a
* fresh, unpatched app.asar.
*
* Failure policy: refuse rather than guess. An unpatched app still runs, it just forgets the
* sign-in; a corrupted app.asar would not start at all. Every unexpected shape is a hard exit
* that leaves the original archive untouched.
*
* asar layout (all little-endian):
* [0] uint32 = 4 size of the next field
* [4] uint32 = headerBufLen size of the header pickle
* [8] uint32 = payloadSize 4 + headerString length, 4-byte aligned
* [12] uint32 = headerStrLen exact JSON length
* [16] utf8 = headerString JSON file tree, padded to a 4-byte boundary
* then file bodies; each node's "offset" is relative to the end of the header.
*/
'use strict';
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const ASAR = process.argv[2] || '/usr/lib/claude-desktop/resources/app.asar';
const MARKER = 'CLAUDE_ADDON_SAFESTORAGE_PATCH';
const PATCH =
`/*${MARKER}*/try{require("electron").safeStorage.setUsePlainTextEncryption(true);}` +
`catch(e){try{console.error("[claude_desktop addon] safeStorage opt-in failed:",e&&e.message);}catch(_){}}`;
const log = (m) => process.stdout.write(`${m}\n`);
const fail = (m) => {
process.stderr.write(`${m}\n`);
process.exit(1);
};
const alignInt = (i, a) => i + ((a - (i % a)) % a);
const sha256 = (b) => crypto.createHash('sha256').update(b).digest('hex');
function readArchive(file) {
const buf = fs.readFileSync(file);
if (buf.length < 16 || buf.readUInt32LE(0) !== 4) fail(`Not an asar archive: ${file}`);
const headerBufLen = buf.readUInt32LE(4);
const payloadSize = buf.readUInt32LE(8);
const headerStrLen = buf.readUInt32LE(12);
const contentBase = 8 + headerBufLen;
if (headerBufLen !== 4 + payloadSize || payloadSize !== 4 + alignInt(headerStrLen, 4)) {
fail(`Corrupt asar header pickle in ${file}`);
}
if (16 + headerStrLen > buf.length || contentBase > buf.length) {
fail(`Corrupt asar: header extends past end of ${file}`);
}
let header;
try {
header = JSON.parse(buf.toString('utf8', 16, 16 + headerStrLen));
} catch (e) {
fail(`Corrupt asar: header is not valid JSON (${e.message})`);
}
return { buf, header, contentBase };
}
/* Every packed leaf, as [path, node]. Nodes flagged `unpacked` live in app.asar.unpacked/ and
* `link` nodes are symlinks — neither has bytes inside the archive, so both are carried through
* untouched and skipped here. */
function packedLeaves(header) {
const out = [];
(function walk(dir, prefix) {
for (const [name, node] of Object.entries(dir.files)) {
const p = prefix ? `${prefix}/${name}` : name;
if (node.files) walk(node, p);
else if (!node.unpacked && typeof node.link !== 'string') out.push([p, node]);
}
})(header, '');
return out;
}
/* Offsets are decimal strings and can exceed 2^31; reject anything that is not a plain, exact,
* in-bounds extent rather than letting parseInt("12junk") or NaN silently slice the wrong bytes. */
function extentOf(ar, node, where) {
if (!/^\d+$/.test(String(node.offset))) fail(`Bad offset for ${where}: ${node.offset}`);
const off = Number(node.offset);
const size = node.size;
if (!Number.isSafeInteger(off)) fail(`Offset out of safe range for ${where}`);
if (!Number.isSafeInteger(size) || size < 0) fail(`Bad size for ${where}: ${size}`);
const start = ar.contentBase + off;
const end = start + size;
if (end > ar.buf.length) fail(`Extent of ${where} runs past end of archive`);
return { start, end };
}
const bodyOf = (ar, node, where) => {
const { start, end } = extentOf(ar, node, where);
return ar.buf.subarray(start, end);
};
function resolve(header, relPath) {
let node = header;
for (const part of relPath.split('/')) {
if (!node.files || !node.files[part]) return null;
node = node.files[part];
}
return node;
}
/* Matches @electron/asar: whole-file hash plus one hash per blockSize chunk. An empty file has
* an empty block list, not a single block over zero bytes. */
function integrityOf(buf, blockSize) {
const blocks = [];
for (let i = 0; i < buf.length; i += blockSize) {
blocks.push(sha256(buf.subarray(i, Math.min(i + blockSize, buf.length))));
}
return { algorithm: 'SHA256', hash: sha256(buf), blockSize, blocks };
}
/* Insert the opt-in after the bundle's leading "use strict" directive. It must go *after* it: a
* directive prologue only takes effect as the very first statement, so prepending would silently
* drop the whole main process out of strict mode.
*
* Returns null — meaning "refuse to patch" — for anything that is not unambiguously a directive.
* `"use strict" + x` is an expression, not a directive, and injecting into it would produce a
* syntax error, so the directive is only accepted when it is terminated by its own semicolon, a
* line break, or end of input. */
function applyPatch(source) {
const m = /^\s*(['"])use strict\1(;?)/.exec(source);
if (!m) return null;
const rest = source.slice(m[0].length);
const terminated = m[2] === ';' || rest === '' || /^[\r\n]/.test(rest);
if (!terminated) return null;
// Supply the terminator when the directive relied on ASI; without it the injected code would
// continue the string-literal expression instead of following it.
const sep = m[2] === ';' ? '' : ';';
return source.slice(0, m[0].length) + sep + PATCH + rest;
}
function writeAll(fd, buf) {
let off = 0;
while (off < buf.length) off += fs.writeSync(fd, buf, off, buf.length - off);
}
/* Re-read the rebuilt archive from disk and prove it is sound before it replaces a working one.
* A short or truncated write late in the file would otherwise still pass a marker-only check,
* because the main bundle sits near the front. */
function verifyRebuilt(file, mainRel, expectedLeafCount) {
const ar = readArchive(file);
const leaves = packedLeaves(ar.header);
if (leaves.length !== expectedLeafCount) {
fail(`Rebuilt archive has ${leaves.length} packed entries, expected ${expectedLeafCount}`);
}
let maxEnd = ar.contentBase;
for (const [p, node] of leaves) {
const { end } = extentOf(ar, node, p);
if (end > maxEnd) maxEnd = end;
}
if (maxEnd !== ar.buf.length) {
fail(`Rebuilt archive is truncated or has ${ar.buf.length - maxEnd} trailing bytes`);
}
const mainNode = resolve(ar.header, mainRel);
if (!mainNode) fail(`Rebuilt archive lost its main entry ${mainRel}`);
const body = bodyOf(ar, mainNode, mainRel);
if (!body.toString('utf8').includes(MARKER)) fail('Rebuilt archive is missing the patch marker');
if (mainNode.integrity && sha256(body) !== mainNode.integrity.hash) {
fail('Rebuilt archive has a stale integrity hash for the main entry');
}
}
function main() {
if (!fs.existsSync(ASAR)) fail(`Missing ${ASAR}`);
const ar = readArchive(ASAR);
const pkgNode = resolve(ar.header, 'package.json');
if (!pkgNode) fail('app.asar has no package.json');
const pkg = JSON.parse(bodyOf(ar, pkgNode, 'package.json').toString('utf8'));
const mainRel = pkg.main;
if (!mainRel) fail('package.json has no "main" entry');
const mainNode = resolve(ar.header, mainRel);
if (!mainNode) fail(`main entry not found in archive: ${mainRel}`);
if (mainNode.unpacked) fail(`main entry ${mainRel} is unpacked; refusing to patch`);
if (pkg.type === 'module') fail(`main entry ${mainRel} is ESM; this patcher emits CommonJS`);
const original = bodyOf(ar, mainNode, mainRel).toString('utf8');
if (original.includes(MARKER)) {
log(`Already patched: ${mainRel}`);
return;
}
const patchedSource = applyPatch(original);
if (patchedSource === null) {
fail(`${mainRel} does not begin with a recognized "use strict" directive; refusing to patch`);
}
const patched = Buffer.from(patchedSource, 'utf8');
/* Rebuild: copy every packed body in tree order, substituting the patched main entry, and
* reassign offsets as we go. asar headers store a byte offset and length per file, so content
* cannot simply grow in place. */
const leaves = packedLeaves(ar.header);
const chunks = [];
let offset = 0;
let patchedCount = 0;
for (const [p, node] of leaves) {
const isMain = node === mainNode;
const body = isMain ? patched : bodyOf(ar, node, p);
node.offset = String(offset);
node.size = body.length;
if (isMain && node.integrity) {
node.integrity = integrityOf(body, node.integrity.blockSize || 4 * 1024 * 1024);
}
offset += body.length;
chunks.push(body);
if (isMain) patchedCount++;
}
if (patchedCount !== 1) fail(`expected to rewrite exactly 1 main entry, rewrote ${patchedCount}`);
const headerString = JSON.stringify(ar.header);
const strLen = Buffer.byteLength(headerString);
const payloadSize = 4 + alignInt(strLen, 4);
const headerBufLen = 4 + payloadSize;
const prefix = Buffer.alloc(16 + alignInt(strLen, 4));
prefix.writeUInt32LE(4, 0);
prefix.writeUInt32LE(headerBufLen, 4);
prefix.writeUInt32LE(payloadSize, 8);
prefix.writeUInt32LE(strLen, 12);
prefix.write(headerString, 16, 'utf8');
/* Write beside the target and rename, so an interrupted run can never leave a torn app.asar.
* The temp name carries the pid so two runs cannot share it, and it is removed on every
* failure path before the rename. */
const dir = path.dirname(ASAR);
const tmp = path.join(dir, `.${path.basename(ASAR)}.addon-tmp.${process.pid}`);
const mode = fs.statSync(ASAR).mode & 0o7777;
try {
const out = fs.openSync(tmp, 'wx', mode);
try {
writeAll(out, prefix);
for (const c of chunks) writeAll(out, c);
fs.fsyncSync(out); // durable before it becomes the live archive
} finally {
fs.closeSync(out);
}
fs.chmodSync(tmp, mode);
verifyRebuilt(tmp, mainRel, leaves.length);
fs.renameSync(tmp, ASAR);
} catch (e) {
try {
fs.unlinkSync(tmp);
} catch (_) {
/* nothing to clean up */
}
fail(`Rebuild failed, original left untouched: ${e.message}`);
}
/* Sync the directory so the rename itself survives a crash, not just the file's contents. */
try {
const dfd = fs.openSync(dir, 'r');
fs.fsyncSync(dfd);
fs.closeSync(dfd);
} catch (_) {
/* best effort */
}
log(`Patched ${mainRel} in ${ASAR} (safeStorage plain-text opt-in)`);
}
main();

View File

@@ -1,7 +1,3 @@
## 3.3.0.1 (2026-07-28)
- Fixed searches failing through ingress with `500 Internal Server Error` (#2906, #2646). Home Assistant's ingress proxy re-encodes the query string and passes a space as `+`, along with a bare `:` `/` `?` `@` `!` `$` `'` `(` `)` `*` `,` - all of which Seerr's OpenAPI validator rejects as reserved characters. Nginx now re-encodes them before proxying, so titles such as `Monsters, Inc.`, `Ocean's Eleven`, `Mission: Impossible` and `Who? What?` search correctly. Only ingress was affected; the directly published port 5055 always worked.
## 3.3.0 (2026-06-05)
- Update to latest version from seerr-team/seerr (changelog : https://github.com/seerr-team/seerr/releases)

View File

@@ -56,7 +56,7 @@ COPY ha_automodules.sh /ha_automodules.sh
RUN chmod 744 /ha_automodules.sh && /ha_automodules.sh "$MODULES" && rm /ha_automodules.sh
# Manual apps
ENV PACKAGES="nginx nginx-mod-http-js"
ENV PACKAGES="nginx"
# Automatic apps & bashio
COPY ha_autoapps.sh /ha_autoapps.sh

View File

@@ -96,4 +96,4 @@ schema:
slug: seerr
udev: true
url: https://github.com/alexbelgium/hassio-addons/tree/master/seerr
version: "3.3.0.1"
version: "3.3.0"

View File

@@ -1,14 +0,0 @@
# Load the njs module used by njs/ingress.js.
#
# The nginx-mod-http-js package ships this same file, but it cannot be relied
# on: .templates/ha_automatic_packages.sh moves the rootfs /etc/nginx aside to
# /etc/nginx2 before installing nginx, then does `rm -r /etc/nginx` and restores
# the saved tree afterwards. That deletes anything the package placed under
# /etc/nginx, including its own load_module snippet, and nginx would then fail
# to start with "unknown directive js_import".
#
# Shipping it in the rootfs makes it survive that swap. The filename matches the
# package's on purpose: if the package's copy is ever kept instead, it overwrites
# this one rather than adding a second load_module for the same module, which
# nginx rejects as a duplicate.
load_module /usr/lib/nginx/modules/ngx_http_js_module.so;

View File

@@ -49,11 +49,6 @@ http {
'' close;
}
# Repair the query string mangled by Supervisor's ingress proxy before it
# reaches Seerr's OpenAPI validator. See njs/ingress.js for the full story.
js_import ingress from /etc/nginx/njs/ingress.js;
js_set $ingress_uri ingress.uri;
include /etc/nginx/includes/resolver.conf;
include /etc/nginx/includes/upstream.conf;

View File

@@ -1,83 +0,0 @@
/*
* Repair the query string of a Home Assistant ingress request.
*
* Supervisor proxies ingress traffic with `params=request.query`
* (supervisor/api/ingress.py), so aiohttp/yarl re-encodes an already-decoded
* query string on the way to this add-on. yarl's "safe" set is much wider than
* the one Seerr's express-openapi-validator will accept: yarl emits a space as
* "+" and passes ":", "/", "?", "@", "!", "$", "'", "(", ")", "*" and ","
* through bare, while the validator checks the raw, still-encoded value against
*
* RESERVED_CHARS = /[\:\/\?#\[\]@!\$&\'()\*\+,;=]/
*
* and answers 400 "Parameter '<name>' must be url encoded". Every search for a
* title containing a space or punctuation therefore fails - "Monsters, Inc.",
* "Ocean's Eleven", "Mission: Impossible" - which Seerr's UI reports as a
* 500. The same requests succeed on the directly published port 5055, which
* does not pass through Supervisor.
*
* "?" deserves a note: the validator strips one with `qs.replace('?', '')`
* before testing, so a single bare "?" slips through by accident and only a
* second one ("Who? What?") produces the 400. It is encoded here regardless.
*
* Re-encoding those characters here is lossless, because yarl only ever emits
* them bare when they were literal characters of the value: anything the user
* actually typed that is ambiguous comes through already percent-encoded
* (a typed "+" arrives as "%2B", "&" as "%26", "=" as "%3D").
*
* "&" and "=" are deliberately NOT re-encoded: they are the query string's own
* separators, so a bare one is always structural.
*/
/*
* Every character of the validator's RESERVED_CHARS except "&" and "=", which
* are the query string's own separators and are handled above. Deriving the
* set from what the validator rejects - rather than from what yarl currently
* emits bare - keeps this correct if either side changes its safe set.
*/
var NEEDS_ENCODING = /[:\/?#\[\]@!$'()*,;]/g;
function encodePart(part) {
return part
/* yarl encodes a space as "+"; a literal "+" arrives as "%2B". */
.replace(/\+/g, "%20")
.replace(NEEDS_ENCODING, function (c) {
return "%" + c.charCodeAt(0).toString(16).toUpperCase();
});
}
/*
* Returns the request URI with the path untouched byte-for-byte and only the
* query string repaired. Used as the proxy_pass target.
*/
function uri(r) {
var raw = r.variables.request_uri;
var split = raw.indexOf("?");
if (split < 0) {
return raw;
}
var path = raw.substring(0, split);
var args = raw.substring(split + 1);
/* A bare trailing "?" is forwarded as-is, so the URI stays byte-for-byte. */
if (args === "") {
return raw;
}
var repaired = args
.split("&")
.map(function (pair) {
var eq = pair.indexOf("=");
if (eq < 0) {
return encodePart(pair);
}
return encodePart(pair.substring(0, eq)) + "=" + encodePart(pair.substring(eq + 1));
})
.join("&");
return path + "?" + repaired;
}
export default { uri };

View File

@@ -10,11 +10,9 @@ server {
location ^~ / {
set $app '%%ingress_entry%%';
# Forward the request URI with the path byte-for-byte as received, and
# the query string re-encoded so the characters Supervisor's ingress
# proxy passes through bare (a space as "+", plus ":/@!$'()*,") do not
# trip Seerr's OpenAPI validator. See njs/ingress.js for the details.
proxy_pass http://127.0.0.1:5055$ingress_uri;
# Forward the raw request URI exactly as received by this nginx.
# This is the safest way to preserve query-string encoding.
proxy_pass http://127.0.0.1:5055$request_uri;
proxy_http_version 1.1;
proxy_set_header Referer $http_referer;

View File

@@ -1,13 +1,3 @@
## 4.16-r0-ls95-7 (28-07-2026)
- Fix Selkies dying with a Rust `RuntimeDirNotSet` unwrap panic just after `Data WebSocket Server listening on port 8081`, and the data websocket then being proxied to the wrong port. Upstream relies on s6-rc ordering: `init-selkies-config` publishes `XDG_RUNTIME_DIR` and `CUSTOM_WS_PORT` into the s6 envdir and `svc-selkies` starts afterwards. The add-on entrypoint replaces s6-overlay and starts every `s6-rc.d` run script in parallel with no dependency graph, so Selkies can snapshot the envdir before that oneshot has written to it -- which is why it bound port 8081 (its own default) instead of the 8082 nginx proxies to, and why its Wayland compositor found no runtime directory to bind a socket in. `20-folders.sh` now exports both variables inside each run script, where no start ordering can lose them, and corrects the base image's `$HOME/.XDG` override where that write happens instead of appending a correction after the `exit 0` that the oneshot-tolerance block adds -- which meant the correction never ran on any boot after the first.
- Microsoft Edge install: `apt-get` and `dpkg` failures no longer abort container startup -- a transient mirror failure or a bad download now logs a warning and leaves the desktop running without Edge, and apt acquisition is bounded so a stalled mirror cannot hang start-up. The post-install wrapper swap is now gated on the helper still being present, so a second run cannot move the installed wrapper aside with nothing left to replace it.
## 4.16-r0-ls95-6 (28-07-2026)
- Share the Selkies startup scripts with the `claude_desktop` add-on by symlink (`20-folders.sh`, `21-gpu_permissions.sh`, `80-configuration.sh`, `90-ingress.sh` and the nginx includes), so the fixes made there now apply here too. This brings in: GPU render-node permissions granted before the graphical services start (fixes `libEGL warning: failed to open /dev/dri/card0: Permission denied` and the resulting "waiting for stream" hang); the s6 envdir and `XDG_RUNTIME_DIR` created up front; the cache redirected to tmpfs; `/tmp/.X11-unix` pre-created so Xorg can bind its socket as a non-root user; the `init-video` and `init-selkies-config` oneshots made non-fatal so a partially permitted device setup no longer crash-loops the add-on; and an ingress config that keeps the correct (non-SSL) nginx server block. The Microsoft Edge install moves to its own webtop-only `81-microsoft_edge.sh`, which also picks up the ownership fixup that previously ran in `20-folders.sh` before Edge was installed and so never matched anything.
- Added support for configuring extra environment variables via the `env_vars` add-on option alongside config.yaml. See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
## 4.16-r0-ls95-5 (2026-02-23)

View File

@@ -138,5 +138,5 @@ slug: webtop-kde
tmpfs: true
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: 4.16-r0-ls95-7
version: 4.16-r0-ls95-5
video: true

View File

@@ -1,13 +1,9 @@
## 4.16-r0-ls93.2 (28-07-2026)
- Fix Selkies dying with a Rust `RuntimeDirNotSet` unwrap panic just after `Data WebSocket Server listening on port 8081`, and the data websocket then being proxied to the wrong port. Upstream relies on s6-rc ordering: `init-selkies-config` publishes `XDG_RUNTIME_DIR` and `CUSTOM_WS_PORT` into the s6 envdir and `svc-selkies` starts afterwards. The add-on entrypoint replaces s6-overlay and starts every `s6-rc.d` run script in parallel with no dependency graph, so Selkies can snapshot the envdir before that oneshot has written to it -- which is why it bound port 8081 (its own default) instead of the 8082 nginx proxies to, and why its Wayland compositor found no runtime directory to bind a socket in. `20-folders.sh` now exports both variables inside each run script, where no start ordering can lose them, and corrects the base image's `$HOME/.XDG` override where that write happens instead of appending a correction after the `exit 0` that the oneshot-tolerance block adds -- which meant the correction never ran on any boot after the first.
- Microsoft Edge install: `apt-get` and `dpkg` failures no longer abort container startup -- a transient mirror failure or a bad download now logs a warning and leaves the desktop running without Edge, and apt acquisition is bounded so a stalled mirror cannot hang start-up. The post-install wrapper swap is now gated on the helper still being present, so a second run cannot move the installed wrapper aside with nothing left to replace it.
## 4.16-r0-ls93.1 (28-07-2026)
- Share the Selkies startup scripts with the `claude_desktop` add-on by symlink (`20-folders.sh`, `21-gpu_permissions.sh`, `80-configuration.sh`, `90-ingress.sh` and the nginx includes), so the fixes made there now apply here too. This brings in: GPU render-node permissions granted before the graphical services start (fixes `libEGL warning: failed to open /dev/dri/card0: Permission denied` and the resulting "waiting for stream" hang); the s6 envdir and `XDG_RUNTIME_DIR` created up front; the cache redirected to tmpfs; `/tmp/.X11-unix` pre-created so Xorg can bind its socket as a non-root user; the `init-video` and `init-selkies-config` oneshots made non-fatal so a partially permitted device setup no longer crash-loops the add-on; and an ingress config that keeps the correct (non-SSL) nginx server block. The Microsoft Edge install moves to its own webtop-only `81-microsoft_edge.sh`, which also picks up the ownership fixup that previously ran in `20-folders.sh` before Edge was installed and so never matched anything.
## 4.16-r0-ls93.1 (2026-07-28)
- Fix crash loop on start: make the `init-video` and `init-selkies-config` oneshots
tolerant of their best-effort device/permission setup failing in the HA add-on
sandbox, so a non-zero exit no longer crash-loops the container and `svc-selkies`
(closes #2918)
## 4.16-r0-ls93 (2026-07-21)
- Update to latest version from linuxserver/docker-webtop (changelog : https://github.com/linuxserver/docker-webtop/releases)

View File

@@ -143,5 +143,5 @@ slug: webtop
tmpfs: true
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "4.16-r0-ls93.2"
version: "4.16-r0-ls93"
video: true

View File

@@ -1 +0,0 @@
../../../../claude_desktop/rootfs/etc/cont-init.d/20-folders.sh

View File

@@ -0,0 +1,96 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
# shellcheck disable=SC2046
set -e
# Define user
PUID=$(bashio::config "PUID")
PGID=$(bashio::config "PGID")
# Set user for microsoft edge if available
if [ -f /usr/bin/microsoft-edge-real ]; then
chown "$PUID:$PGID" /usr/bin/microsoft-edge*
chmod +x /usr/bin/microsoft-edge*
fi
# Check data location
LOCATION=$(bashio::config 'data_location')
if [[ "$LOCATION" = "null" || -z "$LOCATION" ]]; then
# Default location
LOCATION="/config/data_kde"
else
# Check if config is located in an acceptable location
LOCATIONOK=""
for location in "/share" "/config" "/data" "/mnt"; do
if [[ "$LOCATION" == "$location"* ]]; then
LOCATIONOK=true
fi
done
if [ -z "$LOCATIONOK" ]; then
LOCATION="/config/data_kde"
bashio::log.fatal "Your data_location value can only be set in /share, /config or /data (internal to addon). It will be reset to the default location : $LOCATION"
fi
fi
# Set data location
bashio::log.info "Setting data location to $LOCATION"
# Correct home locations
for file in /etc/s6-overlay/s6-rc.d/*/run; do
if [ "$(sed -n '1{/bash/p};q' "$file")" ]; then
sed -i "1a export HOME=$LOCATION" "$file"
sed -i "1a export FM_HOME=$LOCATION" "$file"
fi
done
# Correct home location
for folders in /defaults /etc/cont-init.d /etc/services.d /etc/s6-overlay/s6-rc.d; do
if [ -d "$folders" ]; then
sed -i "s|/config/data_kde|$LOCATION|g" $(find "$folders" -type f) &> /dev/null || true
fi
done
# Change user home
sed -i "s|^\(abc:[^:]*:[^:]*:[^:]*:[^:]*:\)[^:]*|\1$LOCATION|" /etc/passwd
#usermod --home "$LOCATION" abc || true
# Add environment variables
if [ -d /var/run/s6/container_environment ]; then printf "%s" "$LOCATION" > /var/run/s6/container_environment/HOME; fi
if [ -d /var/run/s6/container_environment ]; then printf "%s" "$LOCATION" > /var/run/s6/container_environment/FM_HOME; fi
{
printf "%s\n" "export HOME=\"$LOCATION\""
printf "%s\n" "export FM_HOME=\"$LOCATION\""
} >> ~/.bashrc
# Create folder
echo "Creating $LOCATION"
mkdir -p "$LOCATION"
# Create cache
mkdir -p /.cache
chmod 755 /.cache
if [ -d "/config/.cache" ]; then
cp -rf /config/.cache /.cache
rm -r /config/.cache
fi
ln -sf /config/.cache /.cache
# Set ownership
bashio::log.info "Setting ownership to $PUID:$PGID"
chown -R "$PUID":"$PGID" "$LOCATION"
chmod -R 700 "$LOCATION"
# The Selkies desktop init oneshots do best-effort device/permission setup (mknod
# /dev/input/*, chmod /tmp/selkies*, /dev/dri perms) that is only partially permitted in the
# HA add-on sandbox. A non-zero exit from a oneshot fails add-on bringup and crash-loops the
# container, so make these two tolerant and always report success. Longruns (svc-*) are left
# untouched so s6 keeps supervising them with their real exit codes.
for oneshot in init-video init-selkies-config; do
run="/etc/s6-overlay/s6-rc.d/$oneshot/run"
if [ -f "$run" ] && ! grep -q '^set +e$' "$run"; then
sed -i "1a set +e" "$run"
printf '\nexit 0\n' >> "$run"
fi
done

View File

@@ -1 +0,0 @@
../../../../claude_desktop/rootfs/etc/cont-init.d/21-gpu_permissions.sh

View File

@@ -1 +0,0 @@
../../../../claude_desktop/rootfs/etc/cont-init.d/80-configuration.sh

View File

@@ -0,0 +1,72 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
# shellcheck disable=SC2015
set -e
# Install specific apps
if bashio::config.has_value 'additional_apps'; then
bashio::log.info "Installing additional apps :"
# hadolint ignore=SC2005
NEWAPPS=$(bashio::config 'additional_apps')
for packagestoinstall in ${NEWAPPS//,/ }; do
bashio::log.green "... $packagestoinstall"
if command -v "apk" &> /dev/null; then
apk add --no-cache "$packagestoinstall" &> /dev/null || (bashio::log.fatal "Error : $packagestoinstall not found")
elif command -v "apt" &> /dev/null; then
apt-get install -yqq --no-install-recommends "$packagestoinstall" &> /dev/null || (bashio::log.fatal "Error : $packagestoinstall not found")
elif command -v "pacman" &> /dev/null; then
pacman --noconfirm -S "$packagestoinstall" &> /dev/null || (bashio::log.fatal "Error : $packagestoinstall not found")
fi
done
fi
# Set TZ
if bashio::config.has_value 'TZ'; then
TIMEZONE=$(bashio::config 'TZ')
bashio::log.info "Setting timezone to $TIMEZONE"
ln -snf /usr/share/zoneinfo/"$TIMEZONE" /etc/localtime
echo "$TIMEZONE" > /etc/timezone
fi || (bashio::log.fatal "Error : $TIMEZONE not found. Here is a list of valid timezones : https://manpages.ubuntu.com/manpages/focal/man3/DateTime::TimeZone::Catalog.3pm.html")
# Set keyboard
if bashio::config.has_value 'KEYBOARD'; then
KEYBOARD=$(bashio::config 'KEYBOARD')
bashio::log.info "Setting keyboard to $KEYBOARD"
if [ -d /var/run/s6/container_environment ]; then printf "%s" "$KEYBOARD" > /var/run/s6/container_environment/KEYBOARD; fi
printf "%s\n" "KEYBOARD=\"$KEYBOARD\"" >> ~/.bashrc
fi || true
# Set password
if bashio::config.has_value 'PASSWORD'; then
bashio::log.info "Setting password to the value defined in options"
PASSWORD=$(bashio::config 'PASSWORD')
passwd -d abc
echo -e "$PASSWORD\n$PASSWORD" | passwd abc
elif ! bashio::config.has_value 'PASSWORD' && [[ -n "$(bashio::addon.port "3000")" ]] && [[ -n $(bashio::addon.port "3001") ]]; then
bashio::log.warning "SEVERE RISK IDENTIFIED"
bashio::log.warning "You are opening an external port but your password is not defined"
bashio::log.warning "You risk being hacked ! Please disable the external ports, or use a password"
fi
# Set password
if bashio::config.true 'install_ms_edge'; then
bashio::log.info "Adding microsoft edge"
# Install edge
apt-get update
echo "**** install edge ****"
apt-get install --no-install-recommends -y ca-certificates
if [ -z ${EDGE_VERSION+x} ]; then
EDGE_VERSION=$(curl -sL https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-stable/ \
| awk -F'(<a href="microsoft-edge-stable_|_amd64.deb\")' '/href=/ {print $2}' | sort --version-sort | tail -1)
fi
curl -o /tmp/edge.deb -L "https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-stable/microsoft-edge-stable_${EDGE_VERSION}_amd64.deb"
dpkg -I /tmp/edge.deb
apt-get install --no-install-recommends -y /tmp/edge.deb
echo "**** edge docker tweaks ****"
if [ -f /usr/bin/microsoft-edge-stable ]; then
mv /usr/bin/microsoft-edge-stable /usr/bin/microsoft-edge-real
else
mv /usr/bin/microsoft-edge /usr/bin/microsoft-edge-real
fi
mv /helpers/microsoft-edge-stable /usr/bin/
fi

View File

@@ -1,86 +0,0 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# Webtop-only. Lives here rather than in the shared 80-configuration.sh so the Selkies startup
# scripts stay identical across claude_desktop, webtop and webtop_kde. It also carries the
# ownership fixup that used to sit in 20-folders.sh, which ran before this install and so
# never had anything to match.
if ! bashio::config.true 'install_ms_edge'; then
exit 0
fi
# Every step below is bounded and non-fatal. cont-init.d blocks the whole add-on, so an
# unreachable or stalled packages.microsoft.com -- or a Debian mirror having a bad day -- must
# not hang or kill startup: the desktop is useful without Edge, an add-on stuck before Selkies
# starts is not. `set -e` would turn any apt or dpkg hiccup into exactly that, so each command
# is guarded and every failure path warns and exits 0.
EDGE_DEB=""
edge_giveup() {
bashio::log.warning "$1; skipping the Microsoft Edge install"
if [ -n "$EDGE_DEB" ]; then
rm -f "$EDGE_DEB"
fi
exit 0
}
bashio::log.info "Adding microsoft edge"
# -o Acquire::*Timeout bounds the mirror handshake/transfer the same way --max-time bounds curl.
APT_TIMEOUTS=(-o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 -o Acquire::Retries=1)
apt-get "${APT_TIMEOUTS[@]}" update || edge_giveup "apt-get update failed"
apt-get "${APT_TIMEOUTS[@]}" install --no-install-recommends -y ca-certificates \
|| edge_giveup "Installing ca-certificates failed"
EDGE_REPO="https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-stable"
if [ -z "${EDGE_VERSION+x}" ]; then
EDGE_VERSION=$(curl -sL --fail --connect-timeout 15 --max-time 120 "$EDGE_REPO/" \
| awk -F'(<a href="microsoft-edge-stable_|_amd64.deb\")' '/href=/ {print $2}' | sort --version-sort | tail -1 || true)
fi
if [ -z "$EDGE_VERSION" ]; then
edge_giveup "Could not determine the latest Microsoft Edge version"
fi
# mktemp rather than a fixed /tmp/edge.deb: this runs as root, and a predictable name in a
# world-writable tmpfs is something another process could pre-create as a symlink to redirect
# the download or swap what gets installed.
EDGE_DEB="$(mktemp -t microsoft-edge.XXXXXXXXXX.deb)" || edge_giveup "Could not create a temporary file"
curl -o "$EDGE_DEB" -L --fail --connect-timeout 15 --max-time 600 \
"$EDGE_REPO/microsoft-edge-stable_${EDGE_VERSION}_amd64.deb" \
|| edge_giveup "Downloading Microsoft Edge ${EDGE_VERSION} failed"
dpkg -I "$EDGE_DEB" || edge_giveup "The downloaded Microsoft Edge package is not a valid .deb"
apt-get "${APT_TIMEOUTS[@]}" install --no-install-recommends -y "$EDGE_DEB" \
|| edge_giveup "Installing Microsoft Edge ${EDGE_VERSION} failed"
rm -f "$EDGE_DEB"
bashio::log.info "Applying edge docker tweaks"
# Gated on the helper still being in /helpers, which is where the image ships it and where it
# stops being once installed. Without the guard a second run would move the wrapper already
# sitting in /usr/bin aside as "-real" with nothing left to take its place, and Edge would
# stop launching.
if [ -f /helpers/microsoft-edge-stable ]; then
if [ -f /usr/bin/microsoft-edge-stable ]; then
mv /usr/bin/microsoft-edge-stable /usr/bin/microsoft-edge-real
elif [ -f /usr/bin/microsoft-edge ]; then
mv /usr/bin/microsoft-edge /usr/bin/microsoft-edge-real
fi
mv /helpers/microsoft-edge-stable /usr/bin/
fi
# The wrapper and the real binary must be usable by the desktop user, whose identity
# 20-folders.sh has already settled by the time this runs. Guarded against an empty glob:
# without nullglob the literal pattern would reach chown, and `set -e` would then abort
# container startup rather than just skipping a fixup that has nothing to do.
shopt -s nullglob
edge_binaries=(/usr/bin/microsoft-edge*)
shopt -u nullglob
if [ "${#edge_binaries[@]}" -gt 0 ]; then
chown "$(id -u abc):$(id -g abc)" "${edge_binaries[@]}"
chmod +x "${edge_binaries[@]}"
else
bashio::log.warning "Edge install reported success but no /usr/bin/microsoft-edge* binary is present"
fi

View File

@@ -1 +0,0 @@
../../../../claude_desktop/rootfs/etc/cont-init.d/90-ingress.sh

View File

@@ -0,0 +1,28 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# nginx Path
NGINX_CONFIG=/etc/nginx/sites-available/ingress.conf
SUBFOLDER="$(bashio::addon.ingress_entry)"
# Copy template
cp /defaults/default.conf "${NGINX_CONFIG}"
# Remove ssl part
awk -v n=4 '/server/{n--}; n > 0' "${NGINX_CONFIG}" > tmpfile
mv tmpfile "${NGINX_CONFIG}"
# Remove ipv6
sed -i '/listen \[::\]/d' "${NGINX_CONFIG}"
# Add ingress parameters
sed -i "s|3000|$(bashio::addon.ingress_port)|g" "${NGINX_CONFIG}"
sed -i "s|CWS|8082|g" "${NGINX_CONFIG}"
sed -i "s|SUBFOLDER|/|g" "${NGINX_CONFIG}"
sed -i '/proxy_buffering/a proxy_set_header Accept-Encoding "";' "${NGINX_CONFIG}"
sed -i '/proxy_buffering/a sub_filter_once off;' "${NGINX_CONFIG}"
sed -i '/proxy_buffering/a sub_filter_types *;' "${NGINX_CONFIG}"
sed -i '/proxy_buffering/a sub_filter "vnc/index.html?autoconnect" "vnc/index.html?path=%%path%%/websockify?autoconnect";' "${NGINX_CONFIG}"
sed -i "s|%%path%%|${SUBFOLDER:1}|g" "${NGINX_CONFIG}"
# Enable ingress
cp "${NGINX_CONFIG}" /etc/nginx/sites-enabled

View File

@@ -1 +0,0 @@
../../../../../claude_desktop/rootfs/etc/nginx/includes/mime.types

View File

@@ -0,0 +1,96 @@
types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
application/javascript js;
application/atom+xml atom;
application/rss+xml rss;
text/mathml mml;
text/plain txt;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/x-component htc;
image/png png;
image/svg+xml svg svgz;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/webp webp;
image/x-icon ico;
image/x-jng jng;
image/x-ms-bmp bmp;
font/woff woff;
font/woff2 woff2;
application/java-archive jar war ear;
application/json json;
application/mac-binhex40 hqx;
application/msword doc;
application/pdf pdf;
application/postscript ps eps ai;
application/rtf rtf;
application/vnd.apple.mpegurl m3u8;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/vnd.ms-excel xls;
application/vnd.ms-fontobject eot;
application/vnd.ms-powerpoint ppt;
application/vnd.oasis.opendocument.graphics odg;
application/vnd.oasis.opendocument.presentation odp;
application/vnd.oasis.opendocument.spreadsheet ods;
application/vnd.oasis.opendocument.text odt;
application/vnd.openxmlformats-officedocument.presentationml.presentation
pptx;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
xlsx;
application/vnd.openxmlformats-officedocument.wordprocessingml.document
docx;
application/vnd.wap.wmlc wmlc;
application/x-7z-compressed 7z;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-perl pl pm;
application/x-pilot prc pdb;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert der pem crt;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/xspf+xml xspf;
application/zip zip;
application/octet-stream bin exe dll;
application/octet-stream deb;
application/octet-stream dmg;
application/octet-stream iso img;
application/octet-stream msi msp msm;
audio/midi mid midi kar;
audio/mpeg mp3;
audio/ogg ogg;
audio/x-m4a m4a;
audio/x-realaudio ra;
video/3gpp 3gpp 3gp;
video/mp2t ts;
video/mp4 mp4;
video/mpeg mpeg mpg;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-m4v m4v;
video/x-mng mng;
video/x-ms-asf asx asf;
video/x-ms-wmv wmv;
video/x-msvideo avi;
}

View File

@@ -1 +0,0 @@
../../../../../claude_desktop/rootfs/etc/nginx/includes/proxy_params.conf

View File

@@ -0,0 +1,15 @@
proxy_http_version 1.1;
proxy_ignore_client_abort off;
proxy_read_timeout 86400s;
proxy_redirect off;
proxy_send_timeout 86400s;
proxy_max_temp_file_size 0;
proxy_set_header Accept-Encoding "";
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-NginX-Proxy true;
proxy_set_header X-Real-IP $remote_addr;

View File

@@ -1 +0,0 @@
../../../../../claude_desktop/rootfs/etc/nginx/includes/resolver.conf

View File

@@ -0,0 +1 @@
resolver 127.0.0.11 ipv6=off;

View File

@@ -1 +0,0 @@
../../../../../claude_desktop/rootfs/etc/nginx/includes/server_params.conf

View File

@@ -0,0 +1,6 @@
root /dev/null;
server_name $hostname;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag none;

View File

@@ -1 +0,0 @@
../../../../../claude_desktop/rootfs/etc/nginx/includes/ssl_params.conf

View File

@@ -0,0 +1,9 @@
ssl_protocols TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA;
ssl_ecdh_curve secp384r1;
ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;

View File

@@ -1 +0,0 @@
../../../../../claude_desktop/rootfs/etc/nginx/includes/upstream.conf

View File

@@ -0,0 +1,3 @@
upstream backend {
server 127.0.0.1:8080;
}