Behind a TLS-terminating corporate proxy every HTTPS call inside a container
fails — npm, pip, git, curl, the browser-view pane, and Claude Code's own API
requests. There was no mechanism at all: installing the certificate by hand
inside a container is lost on Reset and had to be repeated per project.
A global CA path in AppSettings with a per-project override on Project, taking
either a single certificate file or a directory. It is bind-mounted read-only
at /tmp/.host-ca (mirroring /tmp/.host-ssh and /tmp/.host-aws) and applied by
entrypoint.sh on every start, so it survives recreation, migration and Reset.
Four things this gets right that are easy to get wrong:
* update-ca-certificates globs *.crt case-sensitively, so a .pem that is merely
copied in is ignored in silence. Certificates are renamed, by
container_cert_name() in Rust and a mirrored few lines of shell.
* The system store only serves curl/git/apt. Node — and so Claude Code itself —
needs NODE_EXTRA_CA_CERTS, Python needs REQUESTS_CA_BUNDLE/SSL_CERT_FILE, and
Chromium reads neither: it wants ~/.pki/nssdb, seeded with certutil
(libnss3-tools, added to the image).
* Those vars are set from Rust at creation, never exported by the entrypoint —
a terminal is a docker exec and sees nothing the entrypoint exported. They are
emitted empty when no CA is configured, since docker commit bakes env into the
snapshot image.
* triple-c.ca-fingerprint hashes the certificate bytes as well as the path, so
a CA rotated in at the same location still forces a recreation.
Verified end to end against a real container and a self-signed CA: curl, node,
python and git all complete a TLS handshake against a server signed by it and
all three fail in the same container without it; the env vars are visible from
a docker exec session; the store is cleaned when the setting is cleared.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSP2KNPhuWKQ4DL5TZEn3k
Four features, plus a latent bug fix.
llama.cpp backend. Claude Code only ever speaks the Anthropic Messages
API — confirmed empirically by pointing it at a logging server, which
received POST /v1/messages?beta=true. llama-server implements that
natively (verified in its README, alongside --port default 8080), so
this is a plain base-URL backend with no translation shim, the same
shape as Ollama. Its --api-key defaults to none, so the auth token is a
placeholder Claude Code requires and llama-server ignores.
Model alias fix. ANTHROPIC_DEFAULT_HAIKU_MODEL is documented as "also
used for background functionality", and Triple-C set none of the alias
vars. So on every custom-endpoint backend, Claude Code resolved `haiku`
to an Anthropic model id and sent it to a local server that does not
have it — background features failed silently. All four
ANTHROPIC_DEFAULT_{OPUS,SONNET,HAIKU,FABLE}_MODEL vars are now pinned to
the backend's configured model, with an optional Haiku override, and
blanked for Anthropic and Bedrock so those keep Claude Code's defaults.
The deprecated ANTHROPIC_SMALL_FAST_MODEL is never emitted. Existing
Ollama and OpenAI-Compatible containers are recreated once so the new
env reaches them; the snapshot is preserved.
Model gateway. Optional LiteLLM sibling container, off by default,
mirroring stt.rs — this is what makes real OpenAI usable, since
api.openai.com has no /v1/messages. Pinned to v1.96.0 by tag and digest:
the 1.82.7/1.82.8 malware was PyPI-only and never affected the official
images, which is precisely why this builds FROM the image rather than
pip-installing, but 1.84.0 is still the floor for proxy CVEs (API-key
SQLi, Host-header auth bypass, MCP auth bypass). Binds 0.0.0.0 because
project containers consume it, and therefore always sets a master_key —
LiteLLM without one accepts any key. The provider key lives in the OS
keychain and is uploaded into a volume, never an image layer or label.
URL relay. A container-side xdg-open/BROWSER shim opens URLs in the
host's browser. Uses an OSC sequence to /dev/tty rather than a printed
sentinel, because the shim usually runs as a grandchild of a process
capturing its children's output. Degrades to printing the URL when no
terminal is attached, so scheduled tasks do not hang. Only http/https,
with control characters rejected before new URL() — which strips
newlines, so java\nscript: would otherwise parse as javascript:. Nothing
auto-opens; the user confirms. The web terminal shows a tap-to-open
banner instead, since that browser may be a phone across a tunnel.
Browser view. A Project Home tab that watches and takes over the browser
Claude drives with Playwright, using Playwright's own dashboard. Zero
image cost — Playwright stays user-installed. It does not reuse the auth
bridge's PortForward, which binds an unauthenticated port: correct for a
throwaway OAuth listener, wrong for mouse and keyboard control of a
browser in a passwordless-sudo container. Instead a token-gated loopback
proxy checks Host, then token or a forbidden-header origin signal,
before a byte reaches the container. Host ports are confined to
47820..=47827 so CSP frame-src can enumerate them rather than widening
to a wildcard, with a test asserting the two agree.
188 frontend tests, 107 Rust tests, both builds clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Project Home (DESIGN-REVIEW §B2): the project is promoted from a 280px
sidebar card to a first-class main-area view. ProjectCard.tsx (1,257
lines) is replaced by a select-only ProjectRow plus tabs for Overview,
Sessions, Automation, Config and Files. The PortMappings, FileManager
and ContainerProgress modals are absorbed rather than reimplemented.
Config gains a Saved/Saving/Failed indicator — save-on-blur failures
previously reached only console.error.
Tier-1 polish (DESIGN-REVIEW §A): new elevation, muted-accent, disabled
and focus-ring tokens; a global :focus-visible ring with every
focus:outline-none removed; filled buttons moved to --accent-emphasis
and white-on-success toggles retired, fixing three WCAG AA failures
(2.1:1, 2.5:1, 2.4:1); a shared Modal primitive with role="dialog",
focus trap and restore, adopted by all remaining modals; status
indicators that carry a glyph and word rather than colour alone.
Ctrl+Shift+W closes a tab, deliberately not Ctrl+W — that is readline's
kill-word, used constantly in the terminal this app is built around.
Auth Bridge: a general loopback-callback bridge so browser logins run
inside a container (aws sso login, Concourse fly login, claude login)
can complete against the host browser. Listeners are discovered from
/proc/net/tcp{,6} — ss/netstat/lsof are absent from the image — bound on
host 127.0.0.1 only, and tunnelled in over the Docker API via socat,
which keeps working on Docker Desktop where container IPs are not
routable. Falls back to [::1] because Node resolves localhost to IPv6
first, so claude login often binds ::1 alone. Opt-in per project.
This extracts create_attached_exec() and moves the existing terminal
session path onto it, so there is one attached-exec implementation
rather than two.
Shared auth token: `claude setup-token` is run in a container, the token
is stored in the OS keychain and injected as CLAUDE_CODE_OAUTH_TOKEN
into Anthropic-backend projects. Contrary to the initial design note,
setup-token uses an Anthropic-hosted redirect and blocks on a stdin
paste prompt rather than a loopback callback, so a stdin command is
required for the flow to complete.
The token is never logged, never returned to the frontend, and is
redacted from the streamed output with a stateful matcher that withholds
any tail that could still grow into a secret. Change detection uses a
random rotation id rather than a hash, since a hash in a docker-inspect
readable label would be an offline verification oracle.
Frontend 33 -> 51 tests; Rust 34 tests. Both builds clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Permission modes: replaces the binary full_permissions flag with a
PermissionMode enum (Plan/Default/AcceptEdits/Bypass). Flag mapping is
defined once in PermissionMode::cli_args() and used by the terminal, the
web terminal, and the scheduler:
Plan -> --permission-mode plan
Default -> (no flag)
AcceptEdits -> --permission-mode acceptEdits
Bypass -> --dangerously-skip-permissions
Choices verified against `claude --permission-mode` on 2.1.226.
full_permissions is retained and effective_permission_mode() falls back
to it, so existing projects.json needs no migration.
Bug fix: triple-c-task-runner ran `claude -p ... --dangerously-skip-
permissions` unconditionally, ignoring the project's setting entirely.
It now reads TRIPLE_C_PERMISSION_MODE, which is injected into the
container, added to the reserved env blocklist, propagated through the
entrypoint's cron env filter, and tracked by a new
triple-c.permission-mode label so a change forces recreation.
Introspection: new commands/inspect_commands.rs exposes read-only views
into the container over docker exec — Claude sessions (parsed from
~/.claude/projects/<cwd>/<uuid>.jsonl), installed capabilities (skills,
agents, commands, hooks, plugins, natively-configured MCP servers), and
the triple-c-scheduler task list, logs and notifications.
Task/session ids are validated against a strict allowlist and every
parameterized call runs as a bare argv vector via bollard, so no shell
is involved. Stopped containers return empty results rather than errors.
No UI yet; that lands with the Project Home view.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Completes the removal begun in the previous commit.
Backend: deletes models/mcp_server.rs, storage/mcp_store.rs and
commands/mcp_commands.rs, the McpStore on AppState, the four IPC
handlers, Project::enabled_mcp_servers, build_mcp_servers_json(),
compute_mcp_fingerprint(), the MCP_SERVERS_JSON env injection, the
mcp-fingerprint label, and the whole MCP container lifecycle.
create_container() and container_needs_recreation() lose their
mcp_servers/network_name parameters.
Container: entrypoint.sh no longer merges MCP_SERVERS_JSON into
~/.claude.json. MCP_SERVERS_JSON stays in the reserved env blocklist.
Security: the Docker socket is no longer auto-mounted for stdio+Docker
MCP servers — it now mounts only when allow_docker_access is set.
Migration: old containers were created with
network_mode=triple-c-net-<projectId> and refuse to start once that
network is gone. docker/network.rs becomes docker/legacy_cleanup.rs with
label-driven, best-effort removal of leftover MCP containers and the
per-project network, called on both delete and recreate.
container_needs_recreation() now forces a rebuild for any container
carrying a non-empty triple-c.mcp-fingerprint label or attached to a
triple-c-net-* network, moving it onto the default bridge. Both can be
dropped a release later.
Docs: drops the MCP sections from README/HOW-TO-USE/TECHNICAL and adds a
short note pointing at Claude Code's native `claude mcp` / `/mcp` /
.mcp.json instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Right-click a tab (or double-click) to rename. Renamed labels show
as "ProjectName: CustomName" and are stored in the project's
renamed_session_names map. The entry is cleared on tab close.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sandbox mode: new per-project toggle that turns on Claude Code's bash
sandbox inside the container. Adds `bubblewrap` and `socat` to the
Dockerfile (the two Linux deps required by the sandbox), and emits a
managed `sandbox` block into `~/.claude/settings.json` via the existing
CLAUDE_CODE_SETTINGS_JSON entrypoint merge:
- `enabled` mirrors the Triple-C toggle and is always emitted, so the
entrypoint's recursive jq merge clears any prior on-state from the
persisted named volume — Triple-C is authoritative.
- `enableWeakerNestedSandbox: true` because we run inside Docker without
privileged user namespaces.
- `allowUnsandboxedCommands: false` to disable the `dangerouslyDisableSandbox`
escape hatch — opting into the sandbox shouldn't come with a runtime
bypass.
When sandbox is on, a SANDBOX_INSTRUCTIONS section is appended to
CLAUDE_INSTRUCTIONS so Claude can guide users through allowing extra
paths/domains, excluding `docker *`/`watchman *` from the sandbox, and
the rule that `sandbox.enabled` is owned by Triple-C. The Claude-Code
settings fingerprint includes sandbox state (only when on, to avoid
spuriously flagging existing containers for recreation on upgrade).
Bedrock service tier: new optional field on the per-project Bedrock
config. When set, exported as ANTHROPIC_BEDROCK_SERVICE_TIER (added in
Claude Code 2.1.122) and included in the Bedrock fingerprint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds first-class support for Claude Code CLI features (2.1.71-2.1.110):
- New ClaudeCodeSettings struct with per-project and global defaults for
TUI mode, effort level, focus mode, thinking summaries, session recap,
auto-scroll, env scrub, and 1-hour prompt caching
- Settings injected as env vars (CLAUDE_CODE_NO_FLICKER, etc.) and
~/.claude/settings.json entries via entrypoint.sh merge block
- New ClaudeCodeSettingsModal component for configuring settings
- Session naming support (-n flag passed to claude CLI, shown in tabs)
- Relaxed reserved prefix filter: CLAUDE_CODE_* env vars now allowed in
custom env vars UI for power users
- Global SSH key path, git name, and git email now used as fallbacks
when per-project values are not set, with UI in SettingsPanel
- Fingerprint-based change detection triggers container recreation when
Claude Code settings change
- Updated README, HOW-TO-USE, and CLAUDE.md documentation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New projects default to standard permission mode (Claude asks before acting).
Existing projects default to full permissions ON, preserving current behavior.
UI toggle uses red/caution styling to highlight the security implications.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reflects that this backend works with any OpenAI API-compatible endpoint
(LiteLLM, OpenRouter, vLLM, text-generation-inference, LocalAI, etc.),
not just LiteLLM. Includes serde aliases for backward compatibility with
existing projects.json files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix serde deserialization error: TypeScript sent "lit_llm" but Rust expected "lite_llm"
- Rename AuthMode enum to Backend across Rust and TypeScript (with serde alias for backward compat)
- Add container image update checking via registry digest comparison
- Improve Settings page: fix image address display spacing, remove per-project auth section
- Update UI labels from "Auth" to "Backend" throughout
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add two new auth modes for projects alongside Anthropic and Bedrock:
- Ollama: connect to local or remote Ollama servers via ANTHROPIC_BASE_URL
- LiteLLM: connect through a LiteLLM proxy gateway to 100+ model providers
Both modes inject ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN env vars into
the container, with optional model override via ANTHROPIC_MODEL. LiteLLM
API keys are stored securely in the OS keychain. Config changes trigger
automatic container recreation via fingerprinting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When enabled, the entrypoint clones mission-control into ~/mission-control
(persisted on the home volume) and symlinks it to /workspace/mission-control.
Flight Control global and project instructions are programmatically appended
to CLAUDE.md. Container recreation is triggered on toggle change.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add double-click-to-rename on project names in the sidebar
- Replace window.confirm() with inline React confirmation for project
removal (confirm dialog didn't block in Tauri webview)
- Add serde(default) to skip_serializing fields in Rust models so
deserialization doesn't fail when frontend omits secret fields
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Model Context Protocol (MCP) server configuration support. Users can
define MCP servers globally (new sidebar tab) and enable them per-project.
Enabled servers are injected into containers as MCP_SERVERS_JSON env var
and merged into ~/.claude.json by the entrypoint.
Backend: McpServer model, McpStore (JSON + atomic writes), 4 CRUD commands,
container injection with fingerprint-based recreation detection.
Frontend: MCP sidebar tab, McpPanel/McpServerCard components, useMcpServers
hook, per-project MCP checkboxes in ProjectCard config.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add per-project port mapping configuration (host:container port pairs with
TCP/UDP protocol) stored in project config and applied as Docker port
bindings at container creation. Port changes trigger automatic container
recreation via fingerprint detection.
- Create PortMappingsModal UI component following the same pattern as
EnvVarsModal, integrated into ProjectCard config panel.
- Inject port mapping details into CLAUDE_INSTRUCTIONS so Claude inside the
container knows which ports are available for testing services.
- Update default global instructions for new installs to encourage use of
subagents for long-running and parallel tasks.
- Replace app icons with new v2 sun logo design for better visibility at
small sizes (taskbar/dock).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the three auth modes (Login, API Key, Bedrock) with two
(Anthropic, Bedrock). The Anthropic mode uses OAuth via `claude login`
inside the terminal, which generates and stores its own API key in the
persistent config volume. The separate API Key mode is removed because
Claude Code now requires interactive approval of externally-provided
keys, making the injected ANTHROPIC_API_KEY approach unreliable.
Old projects stored as "login" or "api_key" are automatically migrated
to "anthropic" via serde aliases.
Also fix the Windows taskbar icon showing as a black square by loading
icon.png instead of icon.ico for the runtime window icon.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Feature 1 - Update Detection: Query Gitea releases API on startup (3s
delay) and every 24h, compare patch versions by platform, show pulsing
"Update" button in TopBar with dialog for release notes/downloads.
Settings: auto-check toggle, manual check, dismiss per-version.
Feature 2 - Multi-Folder Projects: Replace single `path` with
`paths: Vec<ProjectPath>` (host_path + mount_name). Each folder mounts
to `/workspace/{mount_name}`. Auto-migrate old single-path JSON on load.
Container recreation via paths-fingerprint label. AddProjectDialog and
ProjectCard support add/remove/edit of multiple folders.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move git_token and Bedrock credentials to OS keychain instead of
storing in plaintext projects.json via skip_serializing + keyring
- Fix project status stuck in Starting on container creation failure
by resetting to Stopped on any error path
- Add granular store methods to reduce TOCTOU race window
- Add auth_mode, project path, and bedrock config change detection
to container_needs_recreation with label-based fingerprinting
- Fix mutex held across async Docker API call in exec resize by
cloning exec_id under lock then releasing before API call
- Add graceful shutdown via on_window_event to clean up exec sessions
- Extract compute_env_fingerprint and merge_claude_instructions helpers
to eliminate code duplication in container.rs
- Remove unused thiserror dependency
- Return error instead of falling back to CWD when data dir unavailable
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Support per-project environment variables injected into containers,
plus global and per-project Claude Code instructions written to
~/.claude/CLAUDE.md inside the container on start. Reserved env var
prefixes are blocked, and changes trigger automatic container recreation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduces a third auth mode alongside Login and API Key, allowing
projects to authenticate Claude Code via AWS Bedrock. Includes support
for static credentials, profile-based, and bearer-token auth methods
with full UI controls. Also adds a URL accumulator to the terminal to
reassemble long OAuth URLs split across hard newlines, and installs
the AWS CLI v2 in the container image.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tauri v2 desktop app (React/TypeScript + Rust) for managing
containerized Claude Code environments. Includes Gitea Actions
workflow for building and pushing the sandbox container image,
and a BUILDING.md guide for manual app builds on Linux and Windows.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>