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>
Bakes docx, pdf, pptx, xlsx, and frontend-design skills from
github.com/anthropics/skills into /opt/claude-skills/ at image build time
(sparse-checkout, shallow clone). On each chat turn the pipe symlinks them
into $CLAUDE_CONFIG_DIR/skills/ via the new ensure_skills() method.
Symlinks (not copies) mean:
- zero per-chat disk cost regardless of user count;
- image rebuilds that add or update a skill propagate automatically to
existing chats on their next turn (ln -sfn replaces stale targets);
- skills are read-only from the user's perspective — any attempt to
mutate them hits /opt/claude-skills, which is root-owned.
The skill list is surfaced as a valve (SKILLS) so operators can disable
individual skills without a rebuild; it must remain a subset of what the
Dockerfile CLAUDE_SKILLS arg pulled in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#5. In-process _chat_sessions dict is empty after any restart, which
silently started a fresh Claude session on the next turn and lost the prior
conversation. On cache miss we now scan the per-chat CLAUDE_CONFIG_DIR on
disk, pick the newest session JSONL by mtime, and resume from it — Claude's
own filesystem layout is already the source of truth. UUID regex guards
against unexpected files in that directory.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses four hardening items in one change:
- #8 Pin @anthropic-ai/claude-code to 2.1.114 via Dockerfile ARG.
`docker compose build --build-arg CLAUDE_CODE_VERSION=X.Y.Z` upgrades.
- #2 Move `client.kill()` from an `except CancelledError` branch into a
`finally` guarded by an `exited_cleanly` flag. Any abnormal exit path
(httpx timeout, generator GC'd, unexpected exception) now reaps the
open-terminal child — previously only asyncio cancellation did, so
dropped browsers left claude billing tokens until EXECUTE_TIMEOUT.
- #9 Per-chat CLAUDE_CONFIG_DIR=~/chat-<id>/.claude. Two concurrent
chats for the same OWUI user no longer race each other on a shared
~/.claude/. Safe to shard now that credentials live in the proxy
rather than .credentials.json.
- #1 sandbox/cleanup.sh installed at /opt/cleanup.sh. Not auto-run;
README documents dry-run-by-default usage via `docker compose exec`.
CHAT_TTL_DAYS and SESSION_TTL_DAYS control retention.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a Caddy-based reverse proxy (`anthropic-proxy/`) that forwards
/v1/* to api.anthropic.com with the real credential injected at the
proxy, not in the sandbox. The sandbox now only sees
ANTHROPIC_BASE_URL=http://anthropic-proxy:8081 and a placeholder
ANTHROPIC_AUTH_TOKEN, so `env` / `cat /proc/self/environ` yield nothing.
Proxy auth mode:
- CLAUDE_CODE_OAUTH_TOKEN → `Authorization: Bearer` + appends
`oauth-2025-04-20` to anthropic-beta (required for subscription OAuth
on /v1/messages).
- ANTHROPIC_API_KEY → `x-api-key`.
Pipe changes:
- Drop ANTHROPIC_API_KEY / CLAUDE_CODE_OAUTH_TOKEN valves.
- Add ANTHROPIC_BASE_URL valve (defaults to the compose service DNS).
- Replace credential env injection with ANTHROPIC_AUTH_TOKEN=proxied.
Closes acceptance criteria for issue #6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `claude_agent_pipe_sandboxed.py`, a self-contained OpenWebUI pipe
that shells `claude --output-format stream-json` inside an open-webui/
open-terminal container instead of running the Claude Agent SDK
in-process. Each OWUI user gets a dedicated Linux account (mapped via
the X-User-Id header) with a per-chat workspace directory that persists
across turns and whose generated PNG/PDF/CSV artifacts are auto-inlined
into the chat.
Scaffolding under `sandbox/`:
- Dockerfile extending ghcr.io/open-webui/open-terminal with Claude
Code pre-installed system-wide (inherited by every provisioned user)
- docker-compose.yml + README covering standalone deployment
- Async HTTP client + stream-json runner for programmatic use
`sync_pipe.py` pushes the pipe to a running OWUI via the admin
functions API so iteration doesn't require manual repaste.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>