From e100c801c44adffd47d16db2afe80bda27066bbf Mon Sep 17 00:00:00 2001 From: Thomas Friedel Date: Sun, 19 Apr 2026 00:02:04 +0200 Subject: [PATCH] fix(sandbox): expand ~ in CLAUDE_CONFIG_DIR + prune .claude from artifacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit shlex.quote() wraps its argument in single quotes, which suppress tilde expansion. CLAUDE_CONFIG_DIR='~/chat-/.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-/.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) --- claude_agent_pipe_sandboxed.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/claude_agent_pipe_sandboxed.py b/claude_agent_pipe_sandboxed.py index 8b37575..93bb850 100644 --- a/claude_agent_pipe_sandboxed.py +++ b/claude_agent_pipe_sandboxed.py @@ -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-/.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' "