fix(sandbox): expand ~ in CLAUDE_CONFIG_DIR + prune .claude from artifacts

shlex.quote() wraps its argument in single quotes, which suppress tilde
expansion. CLAUDE_CONFIG_DIR='~/chat-<id>/.claude' was being handed to
Claude Code as a literal path beginning with `~`, so the CLI treated it
as a relative directory and created `./~/chat-<id>/.claude/` under cwd.

Concrete symptoms:
  - Session history JSONLs written to the wrong path, silently defeating
    #5 (disk-based session recovery couldn't find them).
  - Per-chat config isolation (#9) silently degraded to a shared dir
    whose actual location depended on whatever cwd the turn ran in.
  - Symlinked skills placed at the correct path were never discovered by
    Claude, because CLAUDE_CONFIG_DIR pointed elsewhere.
  - `.claude.json` artifacts from the stray tree picked up by the
    artifact walker → attempted upload → open-terminal 403 because the
    `~` path segment failed its path-sanity check.

Fix by pre-expanding a leading `~/` to `$HOME/` and double-quoting so
$HOME expands at shell parse time. Also prune `.claude/` and any stray
`~` literal directory from the artifact find() so Claude's internal
state never masquerades as a user artifact going forward.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thomas Friedel 2026-04-19 00:02:04 +02:00
parent 05d53ade55
commit e100c801c4
1 changed files with 18 additions and 2 deletions

View File

@ -360,7 +360,18 @@ def _claude_command(prompt: str, cfg: _ClaudeRunConfig) -> str:
# the same OWUI user race each other on ~/.claude/. With the proxy
# holding credentials, the config dir stores only session history —
# safe to shard per chat and discard with the workdir.
env_parts.append(f"CLAUDE_CONFIG_DIR={shlex.quote(cfg.workdir + '/.claude')}")
#
# Leading `~/` must be pre-expanded to `$HOME/` because shlex.quote()
# adds single quotes, which suppress tilde expansion by the shell.
# Without this, Claude receives a literal `~` as the first path
# component and creates `./~/chat-<id>/.claude/` relative to cwd,
# silently breaking session isolation, session recovery, and skill
# discovery. Double-quoting allows $HOME expansion while still
# escaping any other shell specials in the path.
config_dir = cfg.workdir + "/.claude"
if config_dir.startswith("~/"):
config_dir = "$HOME/" + config_dir[2:]
env_parts.append(f'CLAUDE_CONFIG_DIR="{config_dir}"')
# `claude --dangerously-skip-permissions` still refuses root unless told
# it's sandboxed. open-terminal runs processes as the per-user UID so this
# rarely matters, but setting it is harmless.
@ -569,8 +580,13 @@ async def _snapshot_workspace(
# Use find with -printf to get path + mtime in one call, avoiding N+1
# HTTP hits against /files/list for every subdirectory.
# workdir unquoted so `~` / `$HOME` expand; see OpenTerminalClient.ensure_dir.
# Prune `.claude/` (Claude Code's own config/session storage) and any
# stray `~` literal dir from pre-fix chats — neither is user-facing
# artifact content, and .claude.json would otherwise get inlined as a
# chat attachment.
cmd = (
f"find {workdir} -type f "
f"find {workdir} "
rf"\( -name '.claude' -o -name '~' \) -prune -o -type f "
rf"\( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' "
rf"-o -iname '*.gif' -o -iname '*.svg' -o -iname '*.webp' "
rf"-o -iname '*.pdf' -o -iname '*.csv' -o -iname '*.tsv' "