The SDK pipe hardcoded setting_sources=[], so chats never loaded
~/.claude/CLAUDE.md or any settings.json. Expose it as a valve
(default empty = isolated baseline) so single-user/homelab instances
can opt into persistent context, while shared deployments keep the
safe default.
Parse via _parse_setting_sources(): comma-split, lowercase, drop
unknown tokens so a typo can't silently widen inheritance. README
documents the opt-in plus the hooks/permissions security tradeoff
(CLAUDE.md and settings.json load together — a Claude Code coupling)
and why the valve does not apply to the sandboxed pipe.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
The docx/pdf/pptx/xlsx skills' scripts assume a working set of Python
libraries (pypdf, pdfplumber, reportlab, openpyxl, lxml, defusedxml,
Pillow, markitdown) and native tools (soffice headless, pdftoppm). Without
them, first-turn invocations of those skills fail with ImportError — and
the egress allowlist's pypi.org entry can't save us because pip install
on every turn adds latency and exercises an attack surface we'd rather
not hit on the critical path.
Bakes everything into the image:
- libreoffice-{core,writer,calc,impress}: drops the GUI/Java bloat of
the meta package while keeping headless conversion for all three
office formats. Adds ~500 MB vs ~800+ MB for the full meta.
- poppler-utils + fonts-dejavu: pdftoppm binary, pdf2image backend,
and baseline glyph set so rendered PDFs aren't empty boxes.
- pip: pypdf pdfplumber pdf2image reportlab Pillow openpyxl
python-docx python-pptx pandas numpy matplotlib lxml defusedxml
markitdown[pptx]. Also covers the explicit DS ask (pandas/numpy/
matplotlib) so ad-hoc analysis doesn't hit pip on first use.
- npm -g docx: the JS library the docx skill prefers for generating
richly formatted new documents.
Final image is ~4.6 GB (up from ~1 GB). LibreOffice is the dominant term.
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>
Widens the Bash-subprocess egress allowlist for common agent workflows:
Anthropic direct (api, statsig), Ubuntu mirrors, NodeSource/deb for Node,
crates.io (Rust), yarn, full npm + pypi groups, GitHub release assets,
and wildcards for geoboundaries, googleapis, jsdelivr — covers dataset
fetches and CDN-backed library assets that previously failed silently.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#10. Until now the sandbox container had unrestricted outbound NAT
on sandbox-net — a compromised agent could curl arbitrary hosts, pip/npm
install malicious packages, or exfiltrate files via WebFetch. The
anthropic-proxy path-allowlist (#6) guarded only the credential-injection
proxy, not general egress.
Two layers added, both shipped as managed (enterprise) settings so user
and project settings cannot downgrade them:
- sandbox.* → bubblewrap + local proxy gating Bash subprocesses (and all
their children: pip, npm, git, curl). enableWeakerNestedSandbox=true
because we run inside Docker without privileged user namespaces;
accepted because the Docker container boundary is unchanged and this
adds new egress filtering that previously did not exist.
- permissions.* → deny-by-default WebFetch with explicit domain allows
(WebFetch bypasses the OS sandbox and needs its own allowlist).
failIfUnavailable=true + allowUnsandboxedCommands=false close the two
silent-fallback paths: if bubblewrap/socat install breaks, the container
fails loud instead of serving unsandboxed bash; and the
dangerouslyDisableSandbox escape hatch is disabled so sandbox violations
can't be bypassed via a permission prompt in our unattended deployment.
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>