Drop MCP management; add permission modes, Project Home, Auth Bridge and shared auth #13

Merged
jknapp merged 14 commits from feature/drop-mcp-tab into main 2026-08-10 05:43:09 +00:00
6 changed files with 1018 additions and 161 deletions
Showing only changes of commit cf3b021c72 - Show all commits
+468 -86
View File
@@ -9,16 +9,22 @@ Triple-C (Claude-Code-Container) is a desktop application that runs Claude Code
- [Prerequisites](#prerequisites)
- [First Launch](#first-launch)
- [The Interface](#the-interface)
- [Project Home](#project-home)
- [Project Management](#project-management)
- [Permission Modes](#permission-modes)
- [Project Configuration](#project-configuration)
- [Shared Claude Authentication](#shared-claude-authentication)
- [Browser Logins Inside the Container (Auth Bridge)](#browser-logins-inside-the-container-auth-bridge)
- [AWS Bedrock Configuration](#aws-bedrock-configuration)
- [Ollama Configuration](#ollama-configuration)
- [OpenAI Compatible Configuration](#openai-compatible-configuration)
- [Settings](#settings)
- [Web Terminal (Remote Access)](#web-terminal-remote-access)
- [Terminal Features](#terminal-features)
- [Scheduled Tasks (Inside the Container)](#scheduled-tasks-inside-the-container)
- [Automation & Scheduled Tasks](#automation--scheduled-tasks)
- [Keyboard Shortcuts](#keyboard-shortcuts)
- [What's Inside the Container](#whats-inside-the-container)
- [Claude Code Tips](#claude-code-tips)
- [Troubleshooting](#troubleshooting)
---
@@ -78,7 +84,7 @@ Click **Pull Image** (for Registry/Custom) or **Build Image** (for Local Build).
### 2. Create Your First Project
Switch to the **Projects** tab in the sidebar and click the **+** button.
Switch to the **Projects** tab in the sidebar and click **+ Add**.
1. **Project Name** — Give it a meaningful name (e.g., "my-web-app").
2. **Folders** — Click **Browse** to select a directory on your host machine. This directory will be mounted into the container at `/workspace/<folder-name>`. You can add multiple folders with the **+** button at the bottom of the folder list.
@@ -86,42 +92,61 @@ Switch to the **Projects** tab in the sidebar and click the **+** button.
### 3. Start the Container
Select your project in the sidebar and click **Start**. A progress modal appears showing real-time status as the container starts. The status dot changes from gray (stopped) to orange (starting) to green (running). The modal auto-closes on success.
Click the project in the sidebar. Its **Project Home** opens as a tab in the main area. Click
**Start** in the Project Home header (or use the play control that appears when you hover the
sidebar row).
Progress is reported inline — the sidebar row and the Project Home header show messages like
"Creating container…" and "Starting container…" while the status moves from Stopped (`○`) to
Starting (`◐`) to Running (`●`). Nothing blocks the rest of the app; if something fails you get a
toast with the full detail behind a **Details** disclosure.
### 4. Open a Terminal
Click the **Terminal** button to open an interactive terminal session. A new tab appears in the top bar and an xterm.js terminal loads in the main area.
Click **Open Claude Terminal** in the Project Home header, or press **Ctrl+T**. A new tab appears
in the main tab strip and an xterm.js terminal loads.
Claude Code launches automatically. By default, it runs in standard permission mode and will ask for your approval before executing commands or editing files. To enable auto-approval of all actions within the sandbox, enable **Full Permissions** in the project configuration.
Claude Code launches automatically. The project's **permission mode** decides how much it asks
before acting — the default is to prompt before each tool call. See
[Permission Modes](#permission-modes).
### 5. Authenticate
**Anthropic (OAuth) — default:**
**Anthropic — shared token (recommended):**
Run `claude setup-token` once from **Settings → Claude Authentication** in the sidebar, and every
Anthropic-backend project uses that token without its own login. See
[Shared Claude Authentication](#shared-claude-authentication).
**Anthropic — per-container OAuth:**
1. Type `claude login` or `/login` in the terminal.
2. Claude prints an OAuth URL. Triple-C detects long URLs and shows a clickable toast at the top of the terminal — click **Open** to open it in your browser.
3. Complete the login in your browser. The token is saved and persists across container stops and resets.
3. Complete the login in your browser. The token is saved and persists across container stops, starts and recreations. A **Reset** deletes it — see below.
> If the login hangs after the browser step, the callback could not reach the container. Enable the
> [Auth Bridge](#browser-logins-inside-the-container-auth-bridge) for that project.
**AWS Bedrock:**
1. Stop the container first (settings can only be changed while stopped).
2. In the project card, switch the backend to **Bedrock**.
3. Expand the **Config** panel and fill in your AWS credentials (see [AWS Bedrock Configuration](#aws-bedrock-configuration) below).
1. Stop the container first (most settings can only be changed while stopped).
2. Open the project's **Config** tab and, under **Model**, set **Backend** to **Bedrock**.
3. Fill in your AWS credentials in the same section (see [AWS Bedrock Configuration](#aws-bedrock-configuration) below).
4. Start the container again.
**Ollama:**
1. Stop the container first (settings can only be changed while stopped).
2. In the project card, switch the backend to **Ollama**.
3. Expand the **Config** panel and set the base URL of your Ollama server (defaults to `http://host.docker.internal:11434` for a local instance). Set the **Model ID** to the model you want to use (required).
1. Stop the container first (most settings can only be changed while stopped).
2. Open the project's **Config** tab and, under **Model**, set **Backend** to **Ollama**.
3. Set the base URL of your Ollama server (defaults to `http://host.docker.internal:11434` for a local instance). Set the **Model** to the model you want to use (required).
4. Make sure the model has been pulled in Ollama (e.g., `ollama pull qwen3.5:27b`) or used via Ollama cloud before starting.
5. Start the container again.
**OpenAI Compatible:**
1. Stop the container first (settings can only be changed while stopped).
2. In the project card, switch the backend to **OpenAI Compatible**.
3. Expand the **Config** panel and set the base URL of your OpenAI-compatible endpoint (defaults to `http://host.docker.internal:4000` as an example). Optionally set an API key and model ID.
1. Stop the container first (most settings can only be changed while stopped).
2. Open the project's **Config** tab and, under **Model**, set **Backend** to **OpenAI Compatible**.
3. Set the base URL of your OpenAI-compatible endpoint (defaults to `http://host.docker.internal:4000` as an example). Optionally set an API key and model.
4. Start the container again.
---
@@ -129,22 +154,97 @@ Claude Code launches automatically. By default, it runs in standard permission m
## The Interface
```
┌─────────────────────────────────────────────────────┐
TopBar [ Terminal Tabs ] Docker ● Image ●│
├────────────────────────────────────────────────────┤
│ Sidebar │
│ │ Terminal View
│ Projects │ (xterm.js)
│ Settings
├────────────┴────────────────────────────────────────┤
StatusBar X projects · X running · X terminals
─────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────
[⌂ my-app] [▣ my-app ask] [▣ my-app (bash)] Docker ● Image ● ?
├─────────────┬────────────────────────────────────────────────────────┤
│ Sidebar ┌──────────────────────────────────────────────────┐
│ my-app ● Running · up 2h 5m
│ Projects [Open Claude Terminal] [Shell] [Files]
│ Settings │ │ [Stop] [⋯]
│ │ ├──────────────────────────────────────────────────┤
● my-app │ │ Overview · Sessions · Automation · Config · Files│
│ ○ other │ ├──────────────────────────────────────────────────┤ │
│ │ │ │ │
│ │ │ (Project Home, or a terminal view) │ │
│ │ │ │ │
│ │ └──────────────────────────────────────────────────┘ │
├─────────────┴────────────────────────────────────────────────────────┤
│ 2 project(s) · 1 running · 2 terminal(s) Jump to Current ↓ │
└──────────────────────────────────────────────────────────────────────┘
```
- **TopBar** — Terminal tabs for switching between sessions. Bash shell tabs show a "(bash)" suffix. Status dots on the right show Docker connection (green = connected) and image availability (green = ready).
- **Sidebar** — Toggle between the **Projects** list and the **Settings** panel.
- **Terminal View** — Interactive terminal powered by xterm.js with WebGL rendering. Includes a **Jump to Current** button that appears when you scroll up, so you can quickly return to the latest output.
- **StatusBar** — Counts of total projects, running containers, and open terminal sessions.
- **Tab strip (top)** — One strip holds every open tab, in the order you opened them. There are two
kinds: **Project Home** tabs (`⌂` glyph, project name, status glyph) and **terminal** tabs (`▣`
glyph, plus a small badge showing the permission mode the terminal was launched with —
`plan`, `ask`, `edits` or `bypass`). Bash shell tabs show a "(bash)" suffix. Right-click a
terminal tab to rename it, jump to its project home, or close it; double-click to rename inline.
There is no separate terminal tab bar and no "+" button — tabs appear when you open a project or
a terminal.
- **Status indicators (top right)** — Docker connection and container image availability. Each pairs
a coloured dot with a word, so status is never conveyed by colour alone. The **?** button opens
the built-in help.
- **Sidebar** — Toggle between the **Projects** list and the **Settings** panel. It collapses to a
narrow icon rail with the chevron button, and remembers that choice.
- **Main area** — Shows the active tab: a Project Home view or an xterm.js terminal. With no tabs
open you get a welcome screen with Docker/image/project readiness checks.
- **StatusBar** — Counts of total projects, running containers and open terminal sessions; the
**Jump to Current ↓** button when a terminal is scrolled up; and the microphone button when
speech-to-text is enabled.
---
## Project Home
Clicking a project in the sidebar opens **Project Home** in the main area. The sidebar row is only
for selecting a project and for two quick controls that appear on hover — start/stop, and open a
Claude terminal. Everything else about a project lives in Project Home.
The header shows the project name, its status, how long the container has been up, and the action
buttons. Below that are five tabs:
| Tab | What it's for |
|---|---|
| **Overview** | The permission mode control, a summary of the backend and sandbox settings, capability tiles, recent sessions and scheduled tasks |
| **Sessions** | Past Claude Code conversations stored on this project's config volume, each with a **Resume** button |
| **Automation** | The scheduled tasks running inside this container — see [Automation & Scheduled Tasks](#automation--scheduled-tasks) |
| **Config** | All per-project configuration — see [Project Configuration](#project-configuration) |
| **Files** | Browse, download and upload files inside the container |
### Sessions
Claude Code records each conversation on the project's config volume. The **Sessions** tab lists
them with a name or summary, the session id, its working directory, its age, size and message
count. **Refresh** re-reads the list.
**Resume** opens a new shell tab and runs `claude --resume <session-id>` for you, with the
project's current permission-mode flags applied. The Overview tab shows the four most recent
sessions with the same Resume action.
Sessions can only be read while the container is running, and they are stored on the config volume
— so a **Reset** deletes them.
### Capability Tiles
The Overview tab shows read-only counts of what Claude Code has available **inside this container**:
| Tile | What is counted |
|---|---|
| **Skills** | Directories under `.claude/skills/` that contain a `SKILL.md` |
| **Agents** | `.md` files under `.claude/agents/` |
| **Commands** | `.md` files under `.claude/commands/` |
| **Hooks** | Hook handlers configured in `.claude/settings.json` / `settings.local.json` |
| **Plugins** | Installed and enabled Claude Code plugins |
| **MCP servers** | Servers in `~/.claude.json` and in any `.mcp.json` under `/workspace` |
Both user scope (`/home/claude/.claude`) and project scope (`/workspace/<folder>/.claude`) are
included, and each tile opens a list of what it found.
> **Triple-C does not edit any of this.** Claude Code owns skills, agents, commands, hooks, plugins
> and MCP servers, and it has good built-in tooling for them. The tiles are a window, not an editor:
> **Manage in terminal** opens a terminal in the container so you can use `/agents`, `/hooks`,
> `/plugins`, `/mcp` and friends directly.
The counts are only available while the container is running.
---
@@ -152,59 +252,135 @@ Claude Code launches automatically. By default, it runs in standard permission m
### Project Status
Each project shows a colored status dot:
Each project shows a status glyph paired with a word, so it is readable without relying on colour:
| Color | Status | Meaning |
| Glyph | Status | Meaning |
|-------|--------|---------|
| Gray | Stopped | Container is not running |
| Orange | Starting / Stopping | Container is transitioning |
| Green | Running | Container is active, ready for terminals |
| Red | Error | Something went wrong (check error message) |
| `○` | Stopped | Container is not running |
| `◐` | Starting / Stopping | Container is transitioning (the glyph pulses) |
| `●` | Running | Container is active, ready for terminals |
| `▲` | Error | Something went wrong (check the toast for detail) |
While a container is starting or stopping, the status line is replaced by the live progress message.
### Project Actions
Select a project in the sidebar to see its action buttons:
Most actions live in the **Project Home header**; two live behind the **⋯** overflow menu next to
it. The sidebar row carries only the two hover controls.
| Button | When Available | What It Does |
|--------|---------------|--------------|
| **Start** | Stopped | Creates (if needed) and starts the container |
| **Stop** | Running | Stops the container but preserves its state |
| **Terminal** | Running | Opens a new Claude Code terminal session |
| **Shell** | Running | Opens a bash login shell in the container (no Claude Code) |
| **Files** | Running | Opens the file manager to browse, download, and upload files |
| **Reset** | Stopped | Destroys and recreates the container from scratch |
| **Config** | Always | Toggles the configuration panel |
| **Remove** | Stopped | Deletes the project and its container (with confirmation) |
| Action | Where | When Available | What It Does |
|--------|-------|---------------|--------------|
| **Start** | Project Home header; sidebar hover control | Stopped | Creates (if needed) and starts the container |
| **Stop** | Project Home header; sidebar hover control | Running | Stops the container but preserves its state |
| **Force stop** | Project Home header | Starting / Stopping | Interrupts a transition that is stuck |
| **Open Claude Terminal** | Project Home header; sidebar hover control; `Ctrl+T` | Running | Opens a new Claude Code terminal tab |
| **Shell** | Project Home header | Running | Opens a bash login shell tab in the container (no Claude Code) |
| **Files** | Project Home header, and the **Files** tab | Running | Switches to the Files tab to browse, download and upload files |
| **Config** | The **Config** tab | Always | Per-project configuration (most fields need the container stopped) |
| **Back up container** | **⋯** overflow menu | A container exists | Saves a `.tar.gz` archive of the container to a location you choose |
| **Reset container…** | **⋯** overflow menu | Stopped or Error | Destroys the container, snapshot image and both volumes, then recreates from the base image (wipes `~/.claude`) — asks first |
| **Remove project…** | **⋯** overflow menu | Always | Deletes the project, its container, its volumes and its stored credentials — asks first |
> Both destructive actions confirm before acting, and the Reset dialog spells out what you
> lose: your `claude login`, anything installed inside the container, and every saved
> session transcript. Your mounted project folders live on the host and are not touched.
> The backup archive includes the Claude config volume, which may contain API keys. Keep it private.
### Renaming a Project
Double-click the project name in the sidebar to rename it inline. Press **Enter** to confirm or **Escape** to cancel.
Rename a project in its **Config** tab, under **Workspace → Project name**. Press **Enter** to save
and leave the field, or **Escape** to revert. (Double-clicking a *terminal tab* renames that tab —
that is a different thing.)
### Container Lifecycle
Containers use a **stop/start** model. When you stop a container, everything inside it is preserved — installed packages, modified files, downloaded tools. Starting it again resumes where you left off.
**Reset** removes the container and creates a fresh one. However, your Claude Code configuration (including OAuth tokens from `claude login`) is stored in a separate Docker volume and survives resets.
**Reset container** removes the container, its snapshot image **and both of its named volumes**
(`triple-c-home-<project-id>` and `triple-c-claude-config-<project-id>`), then creates a fresh one
from the clean base image. This is destructive: `~/.claude` and `~/.claude.json` go with the
volumes, so your per-container OAuth login, any skills or agents you installed, your session
transcripts and your scheduled tasks are all lost.
Only **Remove** deletes everything, including the config volume and any stored credentials.
What Reset keeps: your host folders (they are bind mounts and are never touched), the project's
configuration in Triple-C, and anything stored in your OS keychain — including the shared Claude
authentication token. If the project uses that shared token, it re-authenticates by itself after a
Reset; if it relies on `claude login`, you will need to log in again.
Apart from **Remove project…**, Reset is the only action that deletes the volumes. Stopping and
starting preserves them, and so does the automatic container recreation that happens when you
change a setting that affects the container — in both cases your Claude Code configuration
survives.
**Remove project…** deletes everything Reset does, plus the project record itself and its stored
credentials.
### Container Progress Feedback
When starting, stopping, or resetting a container, a progress modal shows real-time status messages (e.g., "Creating container...", "Starting container..."). If an error occurs, the modal displays the error with a **Close** button. A **Force Stop** option is available if the operation stalls. The modal auto-closes on success.
When starting, stopping, or resetting a container, progress is shown inline on the project row and in the Project Home header (e.g., "Creating container...", "Starting container..."), so the rest of the app stays usable. If an error occurs it is raised as a toast with the full detail behind a **Details** disclosure. There is no blocking progress modal.
---
## Permission Modes
Every project has a **permission mode** that decides how much Claude Code does without asking. It
is a segmented control on the **Overview** tab (and again under **Config → Runtime**), and it
replaces the old Full Permissions on/off switch.
| Mode | What Claude does | What Triple-C passes to `claude` |
|------|------------------|----------------------------------|
| **Plan** | Proposes a plan and makes no changes | `--permission-mode plan` |
| **Default** | Asks before each tool call | *(nothing — Claude Code's own default)* |
| **Accept Edits** | Auto-approves file edits; other tools still prompt | `--permission-mode acceptEdits` |
| **Bypass** | Auto-approves every tool call | `--dangerously-skip-permissions` |
New projects start in **Default**. Projects created before permission modes existed keep behaving
the way they did: one that had Full Permissions on becomes **Bypass**, one that had it off becomes
**Default**.
> **CAUTION:** In **Bypass**, Claude can execute any command inside the container without asking.
> The container sandbox limits the blast radius, but think carefully — especially if the container
> has Docker socket access or reaches services on your network. The Overview tab tells you whether
> the in-container sandbox is also on.
### When a change takes effect
- **Terminals** — the mode is applied when a terminal is opened, so it affects terminals you open
from then on. A Claude session that is already running keeps the permissions it started with;
close the tab and open a new terminal to change it. The badge on each terminal tab shows the mode
that terminal was launched with (`plan`, `ask`, `edits`, `bypass`).
- **Resumed sessions** — a session resumed from the **Sessions** tab uses the project's current
mode.
- **Scheduled tasks** — these now honour the permission mode too (they previously always ran with
`--dangerously-skip-permissions`). The mode reaches them through the container's environment,
which can only change when the container is recreated, so **stop and start the project** for a
mode change to reach the scheduler.
> Scheduled tasks run headless (`claude -p`) and cannot answer a permission prompt. In any mode
> other than **Bypass**, a task may simply stop early when Claude Code asks for approval. Its run
> log records which mode it used.
---
## Project Configuration
Click **Config** on a selected project to expand the configuration panel. Settings can only be changed when the container is **stopped** (an orange warning box appears if the container is running).
Open a project's **Config** tab in Project Home. Configuration is grouped into four sections —
**Workspace**, **Model**, **Access** and **Runtime** — plus **Claude instructions** and **Claude
Code settings**.
Changes save automatically when a field loses focus, and a Saved / Saving… / Failed indicator in
the corner tells you what happened. Most settings can only be changed when the container is
**stopped**; a warning chip appears at the top of the tab if it is running. (The project name and
the permission mode can be changed at any time.)
### Mounted Folders
Each project mounts one or more host directories into the container. The mount appears at `/workspace/<mount-name>` inside the container.
- Click **Browse** ("...") to change the host path
- Click **Browse** to change the host path
- Edit the mount name to control where it appears inside `/workspace/`
- Click **+** to add more folders, or **x** to remove one
- Click **+ Add folder** to add more, or **Remove** to drop one (the last remaining folder cannot be removed)
- Mount names must be unique and use only letters, numbers, dashes, underscores, and dots
### SSH Keys
@@ -235,27 +411,31 @@ Available skills include `/mission`, `/flight`, `/leg`, `/agentic-workflow`, `/f
> This setting can only be changed when the container is stopped. Toggling it triggers a container recreation on the next start.
### Full Permissions
### Permission Mode
Toggle **Full Permissions** to allow Claude Code to run with `--dangerously-skip-permissions` inside the container. This is **off by default**.
The **Runtime** section repeats the permission mode control from the Overview tab — see
[Permission Modes](#permission-modes) for what each mode does and when a change takes effect.
When **enabled**, Claude auto-approves all tool calls (file edits, shell commands, etc.) without prompting you. This is the fastest workflow since you won't be interrupted for approvals, and the Docker container provides isolation.
### Sandbox Mode
When **disabled** (default), Claude prompts you for approval before executing each action, giving you fine-grained control over what it does.
> **CAUTION:** Enabling full permissions means Claude can execute any command inside the container without asking. While the container sandbox limits the blast radius, make sure you understand the implications — especially if the container has Docker socket access or network connectivity.
> This setting can only be changed when the container is stopped. It takes effect the next time you open a terminal session.
Toggles Claude Code's in-container bubblewrap isolation. The Overview tab shows the current state
next to the permission mode, because the two together decide how contained a Bypass-mode session
really is.
### Environment Variables
Click **Edit** to open the environment variables modal. Add key-value pairs that will be injected into the container. Per-project variables override global variables with the same key.
Add key-value pairs under **Access → Environment variables**; they are injected into the container.
Per-project variables override global variables with the same key.
> Reserved prefixes (`ANTHROPIC_`, `AWS_`, `GIT_`, `HOST_`, `TRIPLE_C_`) and specific internal variables (`CLAUDE_INSTRUCTIONS`, `CLAUDE_CODE_SETTINGS_JSON`, etc.) are filtered out to prevent conflicts. `CLAUDE_CODE_*` variables are now allowed, so you can set Claude Code feature flags directly (e.g., `CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1`).
> Reserved prefixes (`ANTHROPIC_`, `AWS_`, `GIT_`, `HOST_`, `TRIPLE_C_`) are filtered out to prevent
> conflicts, along with the exact names Triple-C manages itself: `CLAUDE_INSTRUCTIONS`,
> `CLAUDE_CODE_SETTINGS_JSON`, `CLAUDE_CODE_OAUTH_TOKEN`, `MISSION_CONTROL_ENABLED`,
> `TRIPLE_C_PERMISSION_MODE` and `MCP_SERVERS_JSON`. Other `CLAUDE_CODE_*` variables are allowed, so
> you can set Claude Code feature flags directly (e.g., `CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1`).
### Port Mappings
Click **Edit** to map host ports to container ports. This is useful when Claude Code starts a web server or other service inside the container and you want to access it from your host browser.
Under **Access → Port mappings**, map host ports to container ports. This is useful when Claude Code starts a web server or other service inside the container and you want to access it from your host browser.
Each mapping specifies:
- **Host Port** — The port on your machine (1-65535)
@@ -264,11 +444,11 @@ Each mapping specifies:
### Claude Instructions
Click **Edit** to write per-project instructions for Claude Code. These are written to `~/.claude/CLAUDE.md` inside the container and provide project-specific context. If you also have global instructions (in Settings), the global instructions come first, followed by the per-project instructions.
The **Claude instructions** editor at the bottom of the Config tab holds per-project instructions for Claude Code. These are written to `~/.claude/CLAUDE.md` inside the container and provide project-specific context. If you also have global instructions (in Settings), the global instructions come first, followed by the per-project instructions.
### Claude Code Settings
Click **Edit** next to "Claude Code Settings" to configure Claude Code CLI behavior for this project. These settings control how Claude Code operates inside the container:
The **Claude Code settings** editor, also at the bottom of the Config tab, configures Claude Code CLI behavior for this project. These settings control how Claude Code operates inside the container:
| Setting | What It Does |
|---------|-------------|
@@ -299,22 +479,113 @@ Your MCP configuration persists across container stop/start because `~/.claude.j
---
## Shared Claude Authentication
Instead of running `claude login` separately in every container, you can authenticate once and
share the result across projects. Claude Code's `claude setup-token` mints a long-lived token
(roughly a year) that Triple-C stores in your **OS keychain** and injects into containers as
`CLAUDE_CODE_OAUTH_TOKEN`.
This lives in the sidebar under **Settings → Claude Authentication**.
### Signing in
1. Start at least one project whose container is running — the flow borrows that container as a
place to run the CLI. The token it produces is global, not tied to that project.
2. Start the sign-in. Triple-C runs `claude setup-token` inside the container and streams its
output.
3. Claude Code prints an authorization URL. Open it, sign in, and Anthropic's page gives you a
code to copy — this flow finishes on an Anthropic-hosted page, not a local callback.
4. Paste the code back into Triple-C. The token is captured and written straight to the keychain.
Only one sign-in can run at a time, and the whole flow times out after 15 minutes. A long-lived
token requires a Claude subscription; without one, `setup-token` finishes without printing a token
and nothing is stored.
### How the token is used
- It is injected only into projects whose backend is **Anthropic** — it means nothing to Bedrock,
Ollama or an OpenAI-compatible endpoint.
- Each project can opt out under **Config → Model** ("Use the shared Claude token"). Projects are
opted **in** by default, so a single sign-in covers your whole fleet; opt a project out if you
want it pinned to its own `claude login` identity.
- `CLAUDE_CODE_OAUTH_TOKEN` is reserved — you cannot set it yourself as a custom environment
variable, because a hand-set value would silently outrank the stored token.
- A container picks the token up when it is **next started**: acquiring, re-acquiring, revoking or
opting out changes an internal marker that triggers a container recreation on the next start.
Restart your Anthropic-backend containers after signing in.
### Revoking
Revoking deletes the token from your keychain. Containers keep the value they were given until each
is next started, at which point the same recreation clears the variable.
> The token is never shown in the app, never written to a log, and never sent to the frontend.
> While `setup-token` is running, its output is filtered so anything resembling an `sk-ant-`
> secret is masked before it reaches the screen — including a secret split across two chunks of
> output.
---
## Browser Logins Inside the Container (Auth Bridge)
Some CLIs log you in by opening a browser and waiting for the browser to call back to a temporary
web server they started on `localhost`. `claude login`, `aws sso login` and Concourse's
`fly login` all work this way. When the CLI runs inside a container, that `localhost` is the
*container's* — the browser on your host calls back into nothing and the login hangs forever.
The **Auth Bridge** fixes this. It is **opt-in per project** and **off by default**.
### What it does
- Every couple of seconds it looks inside the container for programs listening on the container's
loopback address, and binds **the same port number** on your host. That is the whole trick: the
redirect URL the login provider was handed resolves correctly on both sides.
- Connections are carried into the container over the Docker API, which keeps working on Docker
Desktop where container IP addresses are not reachable from the host.
- It follows whichever address family the container program actually used. This matters in
practice: Node resolves `localhost` to IPv6 first on Linux, so `claude login` frequently listens
on `::1` and nothing else.
- Ports you have already configured as port mappings are left alone. If a host port is already
taken, the bridge reports a conflict and leaves it alone rather than fighting for it — it will
retry on a later pass.
- The bridge is entirely host-side, so turning it on or off never recreates the container. It stops
by itself when the container stops.
### Security
The host side binds **loopback only**`127.0.0.1` and `[::1]`, never a wildcard address. Nothing
on your network can reach a bridged port. Within your own machine, though, a bridged port is
reachable by any local process for as long as the in-container listener exists, and the services
behind it are unauthenticated: they bound loopback precisely because they expected to be reachable
from nowhere else. Only container programs that bound loopback are bridged; anything listening on
all interfaces is deliberately ignored (publishing those is what port mappings are for).
Leave it off unless you need it, and it will not be running.
---
## AWS Bedrock Configuration
To use Claude via AWS Bedrock instead of Anthropic's API, switch the backend to **Bedrock** on the project card.
To use Claude via AWS Bedrock instead of Anthropic's API, set **Backend** to **Bedrock** under
**Config → Model**.
### Authentication Methods
| Method | Fields | Use Case |
|--------|--------|----------|
| **Keys** | Access Key ID, Secret Access Key, Session Token (optional) | Direct credentials — simplest setup |
| **Profile** | AWS Profile name | Uses `~/.aws/config` and `~/.aws/credentials` on the host |
| **Token** | Bearer Token | Temporary bearer token authentication |
| **Static keys** | Access Key ID, Secret Access Key, Session Token (optional) | Direct credentials — simplest setup |
| **Named profile** | AWS Profile name | Uses `~/.aws/config` and `~/.aws/credentials` on the host |
| **Bearer token** | Bearer Token | Temporary bearer token authentication |
With **Named profile**, the SSO session is validated before Claude Code launches, so an expired
session is caught at the start of a terminal rather than mid-task.
### Additional Bedrock Settings
- **AWS Region** — Required. The region where your Bedrock models are deployed (e.g., `us-east-1`).
- **Model ID** — Optional. Override the default Claude model (e.g., `anthropic.claude-sonnet-4-20250514-v1:0`).
- **Service tier** — Optional. Selects a Bedrock service tier.
### Global AWS Defaults
@@ -330,7 +601,7 @@ Per-project settings always override these global defaults.
## Ollama Configuration
To use Claude Code with a local or remote Ollama server, switch the backend to **Ollama** on the project card.
To use Claude Code with a local or remote Ollama server, set **Backend** to **Ollama** under **Config → Model**.
### Settings
@@ -349,7 +620,7 @@ Triple-C sets `ANTHROPIC_BASE_URL` to point Claude Code at your Ollama server in
## OpenAI Compatible Configuration
To use Claude Code through any OpenAI API-compatible endpoint, switch the backend to **OpenAI Compatible** on the project card. This works with any server that exposes an OpenAI-compatible API, including LiteLLM, OpenRouter, vLLM, text-generation-inference, LocalAI, and others.
To use Claude Code through any OpenAI API-compatible endpoint, set **Backend** to **OpenAI Compatible** under **Config → Model**. This works with any server that exposes an OpenAI-compatible API, including LiteLLM, OpenRouter, vLLM, text-generation-inference, LocalAI, and others.
### Settings
@@ -367,7 +638,14 @@ Triple-C sets `ANTHROPIC_BASE_URL` to point Claude Code at your OpenAI-compatibl
## Settings
Access global settings via the **Settings** tab in the sidebar.
Access global settings via the **Settings** tab in the sidebar. The panel is a set of collapsible
sections: **General**, **Claude Authentication**, **Backends**, **Container**, **Git / SSH**,
**Tools** and **Updates**.
### Claude Authentication
Acquire or revoke the shared Claude authentication token — see
[Shared Claude Authentication](#shared-claude-authentication).
### Docker Settings
@@ -462,7 +740,14 @@ The web terminal UI mirrors the desktop app's terminal experience:
### Multiple Sessions
You can open multiple terminal sessions (even for the same project). Each session gets its own tab in the top bar. Click a tab to switch, or click the **x** on a tab to close it. Tabs show the project name (or custom session name if provided), with a "(bash)" suffix for shell sessions.
You can open multiple terminal sessions (even for the same project). Each session gets its own tab
in the main tab strip, alongside any open Project Home tabs. Click a tab to switch, or click the
**×** on a tab to close it. Tabs show the project name (or a custom session name if you set one),
with a "(bash)" suffix for shell sessions and a badge for the permission mode the session was
launched with.
Right-click a terminal tab for **Rename tab**, **Reset name**, **Open project home** and
**Close tab**; double-click it to rename inline.
### Bash Shell Sessions
@@ -490,16 +775,16 @@ You can paste images from your clipboard into the terminal (Ctrl+V / Cmd+V). The
When you scroll up in the terminal to review previous output, a **Jump to Current** button appears in the bottom-right corner. Click it to scroll back to the latest output.
### File Manager
### Files
Click the **Files** button on a running project to open the file manager modal. You can:
The **Files** tab of Project Home browses inside a running container. You can:
- **Browse** the container filesystem starting from `/workspace`, with breadcrumb navigation
- **Download** any file to your host machine via the download button on each file entry
- **Upload** files from your host into the current container directory
- **Browse** the container filesystem, starting at `/workspace`, with breadcrumb navigation
- **Download** any file to your host machine via the **Download** button on each file entry
- **Upload file** from your host into the current container directory
- **Refresh** the directory listing at any time
The file manager shows file names, sizes, and modification dates.
The listing shows file names, sizes, and modification dates.
### Terminal Rendering
@@ -507,9 +792,48 @@ The terminal uses WebGL for hardware-accelerated rendering of the active tab. In
---
## Scheduled Tasks (Inside the Container)
## Automation & Scheduled Tasks
Once inside a running container terminal, you can set up recurring or one-time tasks using `triple-c-scheduler`. Tasks run as separate Claude Code sessions.
Each container can run Claude Code on a schedule — recurring or one-time — through a small
scheduler called `triple-c-scheduler` that lives inside the image. Tasks run as separate,
headless Claude Code invocations (`claude -p "<your prompt>"`) driven by cron.
The **Automation** tab in Project Home is the place to watch and control them; tasks are created
from inside the container with the `triple-c-scheduler` CLI.
### The Automation Tab
With the container running, the Automation tab lists every task the scheduler knows about. For each
one you get its name, whether it is recurring or one-time, its cron expression or scheduled time,
and when it last ran. For each task you can:
| Control | What it does |
|---------|--------------|
| **Toggle** | Enable or disable the task without deleting it |
| **Run now** | Trigger the task immediately, outside its schedule |
| **Log** | Show the tail of that task's run log (last 200 lines) |
| **Remove** | Delete the task, after a confirmation |
**Refresh** re-reads everything from the container.
When tasks finish they leave **notifications**. If any are waiting, a panel appears at the top of
the tab listing each one with its task name, whether it succeeded or failed, how long ago it ran
and a summary. **Clear all** dismisses them. The Overview tab also shows a notification count and
the next few scheduled tasks.
### Permission mode
Scheduled runs use the project's [permission mode](#permission-modes) — they no longer always run
with `--dangerously-skip-permissions`. Because the mode travels into the container as an
environment variable, **stop and start the project** after changing it for the scheduler to see the
change. Remember that a headless run cannot answer a permission prompt, so in any mode other than
**Bypass** a task may stop early when Claude Code asks for approval; the run log records the mode
that was used.
### Creating Tasks (In the Container)
There is no "add task" form in the app. Create tasks from a terminal in the container — either type
the commands yourself in a **Shell** session, or just ask Claude to do it.
### Create a Recurring Task
@@ -525,7 +849,9 @@ triple-c-scheduler add --name "migrate-db" --at "2026-03-05 14:00" --prompt "Run
One-time tasks automatically remove themselves after execution.
### Manage Tasks
### Manage Tasks From the CLI
The CLI still works, and does the same things the Automation tab does:
```bash
triple-c-scheduler list # List all tasks
@@ -558,6 +884,39 @@ By default, tasks run in `/workspace`. Use `--working-dir` to specify a differen
triple-c-scheduler add --name "test" --schedule "0 */6 * * *" --prompt "Run tests" --working-dir /workspace/my-project
```
> Scheduled tasks live on the project's config volume, so a **Reset** deletes them along with
> everything else on that volume.
---
## Keyboard Shortcuts
### Application
| Shortcut | Action |
|----------|--------|
| **Ctrl+T** | Open a new Claude terminal for the current project (nothing happens unless its container is running) |
| **Ctrl+Shift+W** | Close the active tab |
| **Ctrl+Tab** | Switch to the next tab |
| **Ctrl+Shift+Tab** | Switch to the previous tab |
| **Ctrl+1****Ctrl+9** | Jump to the first through ninth tab |
> **Why Ctrl+Shift+W and not Ctrl+W?** `Ctrl+W` is readline's `kill-word` — it deletes the word
> before the cursor, and it is used constantly in the terminal this app is built around. Binding it
> to "close tab" would make the shell unusable, so Triple-C deliberately leaves `Ctrl+W` alone.
### In the Terminal
| Shortcut | Action |
|----------|--------|
| **Ctrl+Shift+C** | Copy the selection, with trailing whitespace trimmed |
| **Ctrl+Shift+Alt+C** | Copy the selection exactly as-is |
| **Ctrl+Shift+V** | Paste |
| **Ctrl+V** | Paste an image from the clipboard into the container |
| **Ctrl+Shift+M** | Toggle speech-to-text recording (when enabled) |
Everything else goes straight through to the program running in the container.
---
## What's Inside the Container
@@ -623,7 +982,7 @@ These features are built into Claude Code and work inside Triple-C containers wi
- Check that the Docker image is "Ready" in Settings.
- Verify that the mounted folder paths exist on your host.
- Look at the error message displayed in the progress modal.
- Read the error toast — the full message is behind its **Details** disclosure.
### OAuth Login URL Not Opening
@@ -631,10 +990,33 @@ These features are built into Claude Code and work inside Triple-C containers wi
- If the toast doesn't appear, try scrolling up in the terminal — the URL may have already been printed.
- You can also manually copy the URL from the terminal output and paste it into your browser.
### A Browser Login Never Completes
You opened the URL, signed in successfully, and the CLI in the terminal is still waiting. The
callback from your browser is landing on your host's `localhost` while the CLI is listening on the
*container's*. Enable the
[Auth Bridge](#browser-logins-inside-the-container-auth-bridge) for that project and try again.
For Claude specifically, the simpler answer is usually
[Shared Claude Authentication](#shared-claude-authentication), which finishes on an Anthropic-hosted
page and needs no callback at all.
### A Scheduled Task Stopped Part-Way Through
Scheduled tasks run headless and cannot answer a permission prompt. If the project is not in
**Bypass** mode, a task will stop when Claude Code asks for approval. Check the task's **Log** in
the Automation tab — it records the permission mode the run used.
### A Permission Mode Change Didn't Apply
- **In a terminal:** the mode is set when the terminal opens. Close the tab and open a new one.
- **For scheduled tasks:** the mode reaches the scheduler through the container's environment. Stop
the project and start it again.
### File Permission Issues
- Triple-C automatically remaps the container user's UID/GID to match your host user, so files created inside the container should have the correct ownership on your host.
- If you see permission errors, try resetting the container (stop, then click **Reset**).
- If you see permission errors, try resetting the container: stop it, then choose **Reset container** from the **⋯** menu in the Project Home header. Note that this wipes `~/.claude`.
### Settings Won't Save
+172 -31
View File
@@ -1,6 +1,6 @@
# Triple-C (Claude-Code-Container)
Triple-C is a cross-platform desktop application that sandboxes Claude Code inside Docker containers. Each project can optionally enable full permissions mode (`--dangerously-skip-permissions`), giving Claude unrestricted access within the sandbox.
Triple-C is a cross-platform desktop application that sandboxes Claude Code inside Docker containers. Each project chooses its own **permission mode** — from Plan (read-only) through to Bypass (`--dangerously-skip-permissions`), which gives Claude unrestricted access within the sandbox.
## Architecture
@@ -13,41 +13,160 @@ Triple-C is a cross-platform desktop application that sandboxes Claude Code insi
```
┌─────────────────────────────────────────────────────┐
│ TopBar (terminal tabs + Docker/Image status)
│ TopBar (MainTabs strip + Docker/Image status + ?)
├────────────┬────────────────────────────────────────┤
│ Sidebar │ Main Content (terminal views)
│ (25% w, │
│ responsive│
│ Sidebar │ Main Content
│ (25% w, │ · Project Home views, or
│ responsive│ · terminal views (xterm.js)
│ min/max) │ │
├────────────┴────────────────────────────────────────┤
│ StatusBar (project/terminal counts)
│ StatusBar (project/terminal counts, STT, scroll)
└─────────────────────────────────────────────────────┘
```
The main area is driven by **one ordered tab strip** (`components/layout/MainTabs.tsx`) holding
two tab kinds: `home:<projectId>` (Project Home) and `term:<sessionId>` (a terminal). There is no
separate terminal tab bar. `activeSessionId` is derived from the active tab key, so exactly one
thing is current at a time.
### Keyboard Shortcuts
Implemented in `hooks/useKeyboardShortcuts.ts` (document-level, capture phase):
| Shortcut | Action |
|---|---|
| `Ctrl+T` | New Claude terminal for the current project (no-op unless it is running) |
| `Ctrl+Shift+W` | Close the active tab |
| `Ctrl+Tab` / `Ctrl+Shift+Tab` | Cycle tabs forward / backward |
| `Ctrl+1``Ctrl+9` | Jump to the nth tab |
`Ctrl+W` is deliberately **not** bound: it is readline's `kill-word`, used constantly in the
terminal this app is built around. Terminal-scoped keys (`Ctrl+Shift+C`, `Ctrl+Shift+Alt+C`,
`Ctrl+Shift+M`) are handled in `TerminalView.tsx`.
### Project Home
Clicking a project row in the sidebar opens **Project Home** in the main area — the per-project
view, with tabs **Overview · Sessions · Automation · Config · Files**. The sidebar row itself is
select-only (plus hover controls for start/stop and opening a terminal); it holds no configuration.
Per-project configuration lives in the Config tab rather than in modals.
| Tab | Contents |
|---|---|
| **Overview** | Permission mode control, sandbox/backend/Docker-access summary, capability tiles, recent sessions, scheduled tasks |
| **Sessions** | Past Claude Code conversations read from the config volume, with **Resume** |
| **Automation** | The container's `triple-c-scheduler` tasks — enable/disable, run now, read logs, remove, and completion notifications |
| **Config** | Workspace (name, folders), Model (backend), Access (SSH, git, env vars, port mappings), Runtime (permission mode, sandbox, Docker access, Mission Control, instructions, Claude Code settings) |
| **Files** | Browse, download and upload files inside the container |
Container start/stop progress is reported inline (on the sidebar row and in the Project Home
header) via the `container-progress` event, and failures surface as toasts. There is no blocking
progress modal.
### Permission Modes
`PermissionMode` in `models/project.rs` replaces the old `full_permissions` boolean. Four states,
mapped to CLI flags by `PermissionMode::cli_args()`:
| Mode | Serialized | CLI args passed to `claude` |
|---|---|---|
| **Plan** | `plan` | `--permission-mode plan` |
| **Default** | `default` | *(none)* |
| **Accept Edits** | `acceptEdits` | `--permission-mode acceptEdits` |
| **Bypass** | `bypass` | `--dangerously-skip-permissions` |
`Project.permission_mode` is `Option<PermissionMode>`; `effective_permission_mode()` falls back to
the legacy `full_permissions` flag (`true` → Bypass) for records written before the change. Changing
the mode affects terminals opened **from then on** — a running `claude` process keeps the argv it
was launched with.
Scheduled tasks honour it too. The mode is injected as `TRIPLE_C_PERMISSION_MODE` (via
`as_env_value()`) and written as the `triple-c.permission-mode` container label; the entrypoint
snapshots it into `~/.claude/scheduler/.env`, and `container/triple-c-task-runner` translates it
back into flags for its headless `claude -p` run. Because it travels as container env, a mode change
only reaches the scheduler after the container is recreated on its next start (the label mismatch
forces that).
### Container Introspection (Capability Tiles)
`list_container_capabilities` (`commands/inspect_commands.rs`) runs a read-only `find`/`jq` script
inside a running container and returns counts plus item lists for **skills, agents, commands, hooks,
plugins and MCP servers**, at user scope (`/home/claude/.claude`) and project scope
(`/workspace/*/.claude`, `/workspace/*/.mcp.json`). Overview renders these as tiles.
Triple-C does not create or edit any of them — Claude Code owns that configuration, and the tiles
link out to a terminal where `/agents`, `/hooks`, `/plugins` and `/mcp` do the real work.
### Auth Bridge
Browser-based logins run *inside* a container (`claude login`, `aws sso login`, Concourse
`fly login`) start an ephemeral HTTP listener on the container's loopback and expect the host
browser's redirect to reach it. `auth_bridge/` closes that gap:
- Listeners are discovered by parsing `/proc/net/tcp{,6}` every 2 seconds — the image ships no
`ss`, `netstat` or `lsof`. Only `TCP_LISTEN` rows bound to loopback are considered; wildcard
binds are deliberately ignored (that is the port-mappings feature's job).
- Each discovered port is bound on the host at **the same port number**, on `127.0.0.1` (required)
and `[::1]` (best effort) — never a wildcard address. Node resolves `localhost` to IPv6 first, so
`claude login` often binds `::1` alone; the bridge follows the family it actually finds.
- Traffic is carried in over the Docker API by an attached exec running `socat`, because container
IPs are not routable from the host on Docker Desktop.
- Ports already covered by the project's port mappings are skipped, and a host port that is already
in use is reported as a conflict rather than fought over.
Opt-in per project (`auth_bridge_enabled`, default `false`), purely host-side, so toggling it never
recreates the container. The poller stops on its own when the container stops.
**Security posture:** the host side binds loopback only. Everything reachable through it is an
unauthenticated service inside the container, so widening those addresses would publish container
internals to the LAN. Nothing else on the network can reach a bridged port.
### Shared Claude Authentication Token
Rather than running `claude login` in every container, `claude setup-token` can be run once
(`commands/auth_token_commands.rs`). The flow borrows a running container, runs the CLI on a PTY,
and the long-lived token it prints is stored in the OS keychain — it is never returned to the
frontend and never logged. Streamed output passes through a chunk-boundary-safe redactor that masks
anything resembling an `sk-ant-` secret.
The token is injected as `CLAUDE_CODE_OAUTH_TOKEN` into every project where the backend is
Anthropic, the project has not opted out (`use_shared_auth_token`, default `true`), and a token is
actually stored. It is a reserved env key, so it cannot be hand-set as a custom variable.
Rotation is tracked with a random id (not a hash of the token) mirrored into the
`triple-c.claude-token-version` label — a hash in a `docker inspect`-readable label would be an
offline verification oracle. Acquiring, rotating, revoking or opting out changes that label, which
forces a container recreation on the next start; that is when a container picks the token up or has
it cleared.
### Container Lifecycle
1. **Create**: New container created with bind mounts, env vars, and labels
2. **Start**: Container started, entrypoint remaps UID/GID, sets up SSH, configures Docker group, injects Claude Code settings
3. **Terminal**: `docker exec` launches Claude Code (or bash shell) with a PTY
4. **Stop**: Container halted (filesystem persists in named volume)
5. **Restart**: Existing container restarted; recreated if settings changed (detected via SHA-256 fingerprint)
6. **Reset**: Container removed and recreated from scratch (named volume preserved)
1. **Create**: New container created with bind mounts, named volumes, env vars, and labels
2. **Start**: Container started, entrypoint remaps UID/GID, sets up SSH, configures Docker group, injects Claude Code settings, rebuilds the scheduler crontab
3. **Terminal**: `docker exec` launches Claude Code (with the project's permission-mode flags) or a bash login shell, with a PTY
4. **Stop**: Container halted (its filesystem layer and both named volumes persist)
5. **Restart**: Existing container restarted; if any `triple-c.*` label no longer matches the project's settings, the container is committed to a snapshot image, removed, and recreated from that snapshot — so installed packages survive
6. **Reset**: Container, snapshot image **and both named volumes** all removed, then recreated from the clean base image. `remove_project_volumes` deletes `triple-c-home-{projectId}` and `triple-c-claude-config-{projectId}`, so `~/.claude`, `~/.claude.json`, the OAuth login, installed skills, session transcripts and the scheduler's tasks are all lost.
### Mounts
| Target in Container | Source | Type | Notes |
|---|---|---|---|
| `/workspace` | Project directory | Bind | Read-write |
| `/home/claude/.claude` | `triple-c-claude-config-{projectId}` | Named Volume | Persists across container recreation |
| `/workspace/<mount-name>` | Each configured project folder | Bind | Read-write; one per folder |
| `/home/claude` | `triple-c-home-{projectId}` | Named Volume | Home directory; survives stop/start and recreation |
| `/home/claude/.claude` | `triple-c-claude-config-{projectId}` | Named Volume | Nested inside the home volume; Docker gives the more specific mount precedence |
| `/tmp/.host-ssh` | SSH key directory | Bind | Read-only; entrypoint copies to `~/.ssh` |
| `/home/claude/.aws` | AWS config directory | Bind | Read-only; for Bedrock auth |
| `/var/run/docker.sock` | Host Docker socket | Bind | If "Allow container spawning" is ON |
These two named volumes are the only ones a project owns. Both are removed by Reset and by project
removal, and by nothing else.
### Authentication Modes
Each project can independently use one of:
- **Anthropic** (OAuth): User runs `claude login` inside the terminal on first use. Token persisted in the config volume across restarts and resets.
- **Anthropic** (OAuth or shared token): either the shared `claude setup-token` token injected as `CLAUDE_CODE_OAUTH_TOKEN` (see below), or a per-container `claude login`. An interactive login's token lives in the config volume and survives container stop/start and recreation — but **not** a Reset, which deletes the volumes.
- **AWS Bedrock**: Per-project AWS credentials (static keys, profile, or bearer token). SSO sessions are validated before launching Claude for Profile auth.
- **Ollama**: Connect to a local or remote Ollama server via `ANTHROPIC_BASE_URL` (e.g., `http://host.docker.internal:11434`). Requires a model ID, and the model must be pulled (or used via Ollama cloud) before starting the container.
- **OpenAI Compatible**: Connect through any OpenAI API-compatible endpoint (LiteLLM, OpenRouter, vLLM, text-generation-inference, LocalAI, etc.) via `ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN`. API key stored securely in OS keychain.
@@ -78,7 +197,7 @@ The web terminal shares the existing `ExecSessionManager` via `Arc`-wrapped stor
### Speech-to-Text (Voice Mode)
Triple-C includes optional speech-to-text powered by [Faster Whisper](https://github.com/SYSTRAN/faster-whisper) running in a separate Docker container. When enabled, a microphone button appears in the bottom-left corner of each terminal view.
Triple-C includes optional speech-to-text powered by [Faster Whisper](https://github.com/SYSTRAN/faster-whisper) running in a separate Docker container. When enabled, a microphone button appears in the StatusBar whenever a terminal session is active.
- **Hotkey**: `Ctrl+Shift+M` to toggle recording
- **Models**: `tiny`, `small`, or `medium` (configurable in Settings)
@@ -101,31 +220,49 @@ Users can override this in Settings via the global `docker_socket_path` option.
| File | Purpose |
|---|---|
| `app/src/App.tsx` | Root layout (TopBar + Sidebar + Main + StatusBar) |
| `app/src/index.css` | Global CSS variables, dark theme, `color-scheme: dark` |
| `app/src/components/layout/TopBar.tsx` | Terminal tabs + Docker/Image status indicators |
| `app/src/components/layout/Sidebar.tsx` | Responsive sidebar (25% width, min 224px, max 320px) |
| `app/src/components/layout/StatusBar.tsx` | Running project/terminal counts |
| `app/src/components/projects/ProjectCard.tsx` | Project config, backend selector, action buttons |
| `app/src/components/projects/ClaudeCodeSettingsModal.tsx` | Claude Code CLI settings modal (TUI mode, effort, focus, caching) |
| `app/src/App.tsx` | Root layout (TopBar + Sidebar + Main + StatusBar + ToastHost) |
| `app/src/index.css` | Global CSS variables, dark theme, `color-scheme: dark`, `:focus-visible` ring |
| `app/src/components/layout/TopBar.tsx` | Hosts MainTabs + Docker/Image status indicators + Help |
| `app/src/components/layout/MainTabs.tsx` | The single main-area tab strip (Project Home + terminal tabs) |
| `app/src/components/layout/Sidebar.tsx` | Responsive sidebar (25% width, min 224px, max 320px), collapsible to an icon rail |
| `app/src/components/layout/StatusBar.tsx` | Project/terminal counts, Jump to Current, STT mic |
| `app/src/components/projects/ProjectRow.tsx` | Select-only sidebar row; opens Project Home, with hover start/stop and terminal controls |
| `app/src/components/projects/ProjectList.tsx` | Project list in sidebar |
| `app/src/components/projects/FileManagerModal.tsx` | File browser modal (browse, download, upload) |
| `app/src/components/projects/ContainerProgressModal.tsx` | Real-time container operation progress |
| `app/src/components/settings/SettingsPanel.tsx` | Docker, AWS, timezone, web terminal, and global settings |
| `app/src/components/projects/PermissionModeControl.tsx` | Plan / Default / Accept Edits / Bypass segmented control |
| `app/src/components/projects/home/ProjectHome.tsx` | Project Home shell: header actions, overflow menu, tab strip |
| `app/src/components/projects/home/OverviewTab.tsx` | Permission mode, summary, capability tiles, recent sessions and tasks |
| `app/src/components/projects/home/SessionsTab.tsx` | Past Claude sessions with Resume |
| `app/src/components/projects/home/AutomationTab.tsx` | Scheduler tasks: toggle, run now, logs, remove, notifications |
| `app/src/components/projects/home/ConfigTab.tsx` | Config sections (Workspace, Model, Access, Runtime) |
| `app/src/components/projects/home/FilesTab.tsx` | File browser (browse, download, upload) |
| `app/src/components/projects/home/CapabilityTiles.tsx` | Read-only skills/agents/commands/hooks/plugins/MCP counts |
| `app/src/components/projects/ClaudeCodeSettingsEditor.tsx` | Claude Code CLI settings (TUI mode, effort, focus, caching) |
| `app/src/components/ui/` | Shared primitives: `Modal`, `Button`, `Toggle`, `Field`, `SegmentedControl`, `StatusIndicator`, `SaveIndicator`, `OverflowMenu`, `ToastHost`, `Tooltip` |
| `app/src/hooks/useKeyboardShortcuts.ts` | `Ctrl+T`, `Ctrl+Shift+W`, `Ctrl+Tab`, `Ctrl+1..9` |
| `app/src/hooks/useContainerProgress.ts` | `container-progress` event → inline progress lines |
| `app/src/components/settings/SettingsPanel.tsx` | Docker, AWS, timezone, web terminal, shared auth, and global settings |
| `app/src/components/settings/SharedAuthSettings.tsx` | Acquire / revoke the shared Claude authentication token |
| `app/src/components/settings/WebTerminalSettings.tsx` | Web terminal toggle, URL, token management |
| `app/src/components/settings/SttSettings.tsx` | STT settings panel (model, port, language, container controls) |
| `app/src/components/terminal/TerminalView.tsx` | xterm.js terminal with WebGL, URL detection, OSC 52 clipboard, image paste |
| `app/src/components/terminal/SttButton.tsx` | Mic button overlay with on-demand container start |
| `app/src/components/terminal/TerminalTabs.tsx` | Tab bar for multiple terminal sessions (claude + bash) |
| `app/src/components/terminal/SttButton.tsx` | Mic button with on-demand STT container start |
| `app/src/hooks/useTerminal.ts` | Terminal session management (claude and bash modes) |
| `app/src/hooks/useProjectActions.ts` | Start/stop/reset/backup and terminal-opening helpers |
| `app/src/hooks/useFileManager.ts` | File manager operations (list, download, upload) |
| `app/src/hooks/useClaudeAuth.ts` | Shared-token status and acquisition |
| `app/src/hooks/useSTT.ts` | Speech-to-text recording, transcription, and container management |
| `app/src-tauri/src/docker/container.rs` | Container creation, mounts, env vars, fingerprinting |
| `app/src-tauri/src/docker/exec.rs` | PTY exec sessions, file upload/download via tar |
| `app/src-tauri/src/docker/container.rs` | Container creation, mounts, env vars, labels, recreation checks, `remove_project_volumes` |
| `app/src-tauri/src/docker/exec.rs` | `create_attached_exec()` — the single attached-exec path; file upload/download via tar |
| `app/src-tauri/src/docker/image.rs` | Image building/pulling |
| `app/src-tauri/src/docker/stt.rs` | Speech-to-text container lifecycle |
| `app/src-tauri/src/docker/legacy_cleanup.rs` | One-release migration shim removing leftovers from the deleted MCP feature |
| `app/src-tauri/src/auth_bridge/` | Loopback callback bridge (`mod.rs`, `proc_net.rs`, `tunnel.rs`) |
| `app/src-tauri/src/commands/project_commands.rs` | Start/stop/rebuild Tauri command handlers |
| `app/src-tauri/src/commands/inspect_commands.rs` | Read-only container views: sessions, capabilities, scheduler tasks |
| `app/src-tauri/src/commands/auth_token_commands.rs` | `claude setup-token` flow, redaction, keychain storage |
| `app/src-tauri/src/commands/auth_bridge_commands.rs` | Auth bridge enable/status commands |
| `app/src-tauri/src/commands/file_commands.rs` | File manager Tauri commands (list, download, upload) |
| `app/src-tauri/src/models/project.rs` | Project struct (backend, Docker access, Claude Code settings, Mission Control) |
| `app/src-tauri/src/models/project.rs` | Project struct (backend, `PermissionMode`, Docker access, Claude Code settings, Mission Control, auth bridge, shared-token opt-out) |
| `app/src-tauri/src/models/app_settings.rs` | Global settings (image source, Docker socket, AWS, Claude Code settings, web terminal, STT) |
| `app/src-tauri/src/web_terminal/server.rs` | Axum HTTP+WS server for remote terminal access |
| `app/src-tauri/src/web_terminal/ws_handler.rs` | WebSocket connection handler and session management |
@@ -140,6 +277,10 @@ Users can override this in Settings via the global `docker_socket_path` option.
| `container/entrypoint.sh` | UID/GID remap, SSH setup, Docker group config, Claude Code settings injection, Mission Control setup |
| `container/osc52-clipboard` | Clipboard shim (xclip/xsel/pbcopy via OSC 52) |
| `container/audio-shim` | Audio capture shim (rec/arecord via FIFO) for voice mode |
| `container/triple-c-scheduler` | Bash CLI managing scheduled task JSON and the crontab |
| `container/triple-c-task-runner` | Cron entry point; maps `TRIPLE_C_PERMISSION_MODE` to flags and runs `claude -p` |
| `container/triple-c-sso-refresh` | AWS SSO session refresh helper |
| `app/src-tauri/src/storage/secure.rs` | OS keychain access (per-project secrets, shared token, rotation id) |
## CSS / Styling Notes
+240 -42
View File
@@ -2,7 +2,7 @@
## Overview
Triple-C (Claude-Code-Container) sandboxes Claude Code inside Docker containers so that when running with `--dangerously-skip-permissions`, Claude only has access to files and projects you explicitly provide. The project consists of two components: a **Docker container image** pre-loaded with development tools, and a **cross-platform desktop application** for managing project containers, terminal sessions, and authentication.
Triple-C (Claude-Code-Container) sandboxes Claude Code inside Docker containers so that even in its most permissive mode — `--dangerously-skip-permissions` Claude only has access to files and projects you explicitly provide. The project consists of two components: a **Docker container image** pre-loaded with development tools, and a **cross-platform desktop application** for managing project containers, terminal sessions, and authentication.
---
@@ -123,7 +123,8 @@ Implementation gotchas for the terminal view and its global controls (merged in
┌──────────────────────────────────────────────────────────┐
│ Docker Container (per project) │
│ │
│ /workspace ←─ bind mount ─► Host project directory
│ /workspace/<name> ←─ bind mount ─► Host project folder
│ /home/claude ←── named volume (home dir) │
│ /home/claude/.claude ←── named volume (persists config) │
│ /tmp/.host-ssh ←── read-only bind mount (SSH keys) │
│ /var/run/docker.sock ←── optional (sibling containers) │
@@ -160,22 +161,143 @@ Terminal resize follows the same pattern: `ResizeObserver` detects container siz
Containers follow a **stop/start** model, not create/destroy:
1. **First start**: A new container is created with bind mounts, environment variables, and labels. The entrypoint remaps UID/GID, configures SSH and git, then runs `sleep infinity` to keep the container alive.
2. **Terminal open**: `docker exec` launches `claude --dangerously-skip-permissions` with a PTY in the running container.
1. **First start**: A new container is created with bind mounts, named volumes, environment variables, and labels. The entrypoint remaps UID/GID, configures SSH and git, rebuilds the scheduler crontab, then runs `sleep infinity` to keep the container alive.
2. **Terminal open**: `docker exec` launches `claude` with a PTY in the running container, with the permission-mode flags from `PermissionMode::cli_args()` (or `bash -l` for a shell session).
3. **Stop**: `docker stop` halts the container but preserves its filesystem. Any packages Claude installed via `apt`, `pip`, `cargo`, etc. survive.
4. **Restart**: `docker start` resumes the existing container. All installed tools and configuration persist.
5. **Reset**: The container is removed and recreated from the image. This is a clean slate — the nuclear option when the container state is corrupted.
4. **Restart**: `docker start` resumes the existing container — unless `container_needs_recreation()` finds a `triple-c.*` label that no longer matches the project's settings, in which case the container is committed to a snapshot image (`triple-c-snapshot-{projectId}:latest`), removed, and recreated from that snapshot. Installed tools survive; the named volumes are untouched.
5. **Reset**: `rebuild_project_container` closes live exec sessions, removes the container, removes the snapshot image, calls `remove_project_volumes` to delete **both** named volumes, then starts fresh from the clean base image.
The `.claude` configuration directory uses a **named Docker volume** (`triple-c-claude-config-{projectId}`) so OAuth tokens from `claude login` persist even across container resets.
Two named volumes exist per project and they are the only ones it owns:
| Volume | Mount point | Purpose |
|---|---|---|
| `triple-c-home-{projectId}` | `/home/claude` | Home directory — `~/.claude.json`, `~/.local`, `~/.ssh`, `~/.aws` |
| `triple-c-claude-config-{projectId}` | `/home/claude/.claude` | Claude Code config: OAuth credential, settings, skills/agents/commands, session transcripts, scheduler state. Nested inside the home volume; Docker gives the more specific mount precedence. |
`remove_project_volumes` names those two volumes explicitly (no prefix sweep) and is called from
exactly two places: `remove_project` and `rebuild_project_container`. Ordinary container removal
passes `v: false`, so stop/start and recreation never touch the volumes — **only Reset and project
removal delete them.** A Reset therefore destroys the `claude login` credential, installed skills,
session transcripts and scheduled tasks; it does not touch host bind mounts, the project record, or
host keychain secrets.
### Permission Modes
`PermissionMode` (`models/project.rs`) is a four-state enum replacing the earlier `full_permissions`
boolean. It reaches Claude Code by two different routes:
| Mode | `cli_args()` — interactive terminals | `as_env_value()` — scheduler |
|---|---|---|
| `Plan` | `--permission-mode plan` | `plan` |
| `Default` | *(no flag)* | `default` |
| `AcceptEdits` | `--permission-mode acceptEdits` | `acceptEdits` |
| `Bypass` | `--dangerously-skip-permissions` | `bypass` |
`Project.permission_mode` is `Option<PermissionMode>`, and `effective_permission_mode()` resolves
`None` from the legacy `full_permissions` flag, so records written before the change keep behaving
the same way.
**Interactive path.** `build_terminal_cmd()` evaluates `cli_args()` when a session is created, so
the flags are fixed for the life of that `claude` process. Changing the mode affects terminals
opened afterwards, not running ones. The same applies to `resume_session_command`, which builds
`claude <flags> --resume <id>` server-side.
**Scheduler path.** Cron jobs run with a minimal environment, so the mode travels as
`TRIPLE_C_PERMISSION_MODE` in the container's env; the entrypoint snapshots the allowlisted
variables into `~/.claude/scheduler/.env`, and `triple-c-task-runner` sources that file and maps the
value back to flags for its `claude -p` run. Container env can only change at create time, so
`container_needs_recreation()` compares a `triple-c.permission-mode` label and forces a recreation
on the next start. A mode change therefore reaches new terminals immediately but the scheduler only
after a stop/start. `TRIPLE_C_PERMISSION_MODE` is a reserved env key so it cannot be hand-set.
### Authentication Modes
Each project independently chooses one of two authentication methods:
Each project independently chooses one backend:
| Mode | How It Works | When to Use |
| Backend | How It Works | When to Use |
|------|-------------|-------------|
| **Anthropic (OAuth)** | User runs `claude login` or `/login` inside the terminal. OAuth URL opens in host browser via URL detection. Token persists in the `.claude` config volume. | Default — personal and team use |
| **AWS Bedrock** | Per-project AWS credentials (static keys, profile, or bearer token) injected as env vars. `~/.aws` config optionally bind-mounted read-only. | Enterprise environments using Bedrock |
| **Anthropic** | Either the shared `CLAUDE_CODE_OAUTH_TOKEN` injected from the OS keychain, or a per-container `claude login` whose credential persists in the `.claude` config volume. The OAuth URL opens in the host browser via URL detection. | Default — personal and team use |
| **AWS Bedrock** | Per-project AWS credentials (static keys, named profile, or bearer token) injected as env vars. `~/.aws` config optionally bind-mounted read-only; SSO sessions are validated before launching Claude for profile auth. | Enterprise environments using Bedrock |
| **Ollama** | `ANTHROPIC_BASE_URL` points at an Ollama server; `ANTHROPIC_AUTH_TOKEN` is set to a placeholder. | Local models (best-effort) |
| **OpenAI Compatible** | `ANTHROPIC_BASE_URL` plus `ANTHROPIC_AUTH_TOKEN` point at any OpenAI-compatible endpoint (LiteLLM, OpenRouter, vLLM, …). | Gateways and proxies (best-effort) |
### Shared Claude Authentication Token
`commands/auth_token_commands.rs` runs `claude setup-token` on a PTY inside a running container.
Contrary to the loopback pattern most CLI logins use, `setup-token` redirects to an Anthropic-hosted
page and then blocks on a stdin paste prompt, so the flow needs a way to feed the pasted code back
in — hence `submit_claude_token_code`. The flow is single-flight (the token is global, so two
concurrent logins would race to overwrite each other's keychain entry) and times out after 15
minutes.
- **Storage** — the OS keychain, under a dedicated service name; the token is never returned to the
frontend, never written to a log, and no command accepts or returns it.
- **Redaction** — streamed output is stripped of ANSI sequences and passed through a stateful
redactor that masks anything matching `sk-ant-` with a plausible body, withholding any tail that
could still grow into a secret across a chunk boundary.
- **Injection** — `CLAUDE_CODE_OAUTH_TOKEN` is set only when the backend is Anthropic, the project
has not opted out (`use_shared_auth_token`, default `true`), and a non-blank token is stored. When
those conditions do not hold, the variable is explicitly set to empty rather than omitted, so a
value baked into a snapshot image by `docker commit` is actively cleared.
- **Rotation** — a random UUID minted on each store is mirrored into the
`triple-c.claude-token-version` label. It is deliberately *not* a hash of the token: labels are
readable by anything that can run `docker inspect`, and a hash would be an offline verification
oracle. A label mismatch forces container recreation on the next start, which is when a container
picks up or loses the token.
### Auth Bridge
CLIs that log in through a browser (`claude login`, `aws sso login`, `fly login`) start an ephemeral
HTTP listener on an unpredictable loopback port and hand the provider a `http://localhost:<port>/…`
redirect. Run inside a container, that listener is unreachable from the host browser and nothing can
be pre-published at container-creation time. `auth_bridge/` bridges it at runtime:
- **Discovery** (`proc_net.rs`) — a `docker exec` reads `/proc/net/tcp` and `/proc/net/tcp6` every
two seconds. The image ships no `ss`, `netstat` or `lsof`. Only rows in state `0A` (`TCP_LISTEN`)
bound to loopback are kept; wildcard binds are ignored on purpose, since publishing those is the
port-mappings feature's job.
- **Family handling** — a `::1`-only listener genuinely cannot be reached over `127.0.0.1`, and Node
resolves `localhost` to IPv6 first on Linux, so `claude login` frequently binds `::1` alone. The
socat target follows the family actually observed; IPv4-mapped rows in `/proc/net/tcp6` are
treated as IPv4.
- **Host bind** (`tunnel.rs`) — the same port number is bound on the host: `127.0.0.1` is required,
`[::1]` is best-effort. **The host side binds loopback only, never a wildcard address**
everything behind it is an unauthenticated in-container service that bound loopback precisely
because it expected to be unreachable.
- **Transport** — each accepted connection is proxied by an attached exec running
`socat - TCP:127.0.0.1:<port>`, because container IPs are not routable from the host under Docker
Desktop. It goes through the same `create_attached_exec()` helper as terminal sessions, with
`tty: false` so socat's stderr is demultiplexed away from the proxied byte stream.
- **Policy** — ports appearing in the project's port mappings are skipped, and a host bind failure
is recorded as a conflict and retried later rather than fought over.
- **Lifecycle** — opt-in per project (`auth_bridge_enabled`, default `false`). It is purely
host-side, so it deliberately has no container-recreation label. The poller stops itself when the
project is gone, the flag is cleared, or the container is no longer running, and `stop()` awaits
it so host ports are provably released.
### Container Introspection
`list_container_capabilities` (`commands/inspect_commands.rs`) executes a read-only shell script in
a running container and returns counts and item lists for skills, agents, commands, hooks, plugins
and MCP servers, across user scope (`/home/claude/.claude`) and project scope
(`/workspace/*/.claude`, `/workspace/*/.mcp.json`). Everything is computed in-container with
`find`/`awk`/`jq`; only the JSON summary crosses the wire, and a stopped container yields zeros
rather than an error.
The script writes nothing. Claude Code owns this configuration and has its own tooling for it
(`/agents`, `/hooks`, `/plugins`, `/mcp`); Triple-C surfaces counts and opens a terminal rather than
rebuilding those editors as forms. `list_claude_sessions` and the scheduler commands
(`list_scheduled_tasks`, `get_scheduled_task_log`, `set_scheduled_task_enabled`,
`run_scheduled_task_now`, `remove_scheduled_task`, `clear_scheduler_notifications`) live in the same
module; the mutating ones shell out to `triple-c-scheduler` rather than editing its state files.
### Main-Area Tab Model
The frontend keeps a single ordered `tabOrder` array in the Zustand store holding two tab kinds,
`home:<projectId>` and `term:<sessionId>`, rendered by `components/layout/MainTabs.tsx`.
`activeSessionId` is *derived* from `activeTabKey`, so exactly one thing is current and a Project
Home tab and a terminal cannot both claim focus. Project configuration is a main-area view
(`components/projects/home/`), not a modal; the sidebar row is select-only.
### UID/GID Remapping
@@ -205,10 +327,12 @@ This avoids the common Docker problem where bind-mount permissions can't be chan
| Data | Storage | Location |
|------|---------|----------|
| Project configurations | JSON file (atomic writes) | `~/.local/share/triple-c/projects.json` |
| API keys | OS keychain | macOS Keychain / Windows Credential Manager / Linux Secret Service |
| API keys and per-project secrets | OS keychain | macOS Keychain / Windows Credential Manager / Linux Secret Service |
| Shared Claude token + rotation id | OS keychain | Separate service entries; never on disk, never in a label |
| App settings | Tauri plugin-store | App data directory |
| Claude config/tokens | Named Docker volume | `triple-c-claude-config-{projectId}` |
| Container filesystem | Docker container layer | Preserved across stop/start, cleared on reset |
| Claude config, sessions, scheduler state | Named Docker volume | `triple-c-claude-config-{projectId}` |
| Container home directory | Named Docker volume | `triple-c-home-{projectId}` |
| Container filesystem | Docker container layer, preserved into `triple-c-snapshot-{projectId}:latest` on recreation | Survives stop/start and recreation; destroyed by Reset |
The projects store uses **atomic writes** (write to `.json.tmp`, then `rename()`) to prevent data corruption if the app crashes mid-write. Corrupted files are backed up to `.json.bak` before being replaced.
@@ -230,92 +354,159 @@ The `TerminalView` component works around this with a **URL accumulator**:
triple-c/
├── README.md # Architecture overview
├── TECHNICAL.md # This document
├── HOW-TO-USE.md # User guide
├── HOW-TO-USE.md # User guide (also served by the in-app Help dialog)
├── BUILDING.md # Build instructions
├── CLAUDE.md # Claude Code instructions
├── DESIGN-REVIEW.md # UI/UX review notes
├── ROADMAP.md # Planned work
├── container/
├── container/ # Sandbox image
│ ├── Dockerfile # Ubuntu 24.04 + all dev tools + Claude Code
│ ├── entrypoint.sh # UID/GID remap, SSH setup, git config, settings injection
│ ├── entrypoint.sh # UID/GID remap, SSH setup, git config, settings injection,
│ │ # scheduler env snapshot + crontab rebuild
│ ├── osc52-clipboard # Clipboard shim (xclip/xsel/pbcopy via OSC 52)
│ ├── audio-shim # Audio capture shim (rec/arecord via FIFO)
│ ├── triple-c-scheduler # Bash-based cron task system
── triple-c-task-runner # Task execution runner for scheduler
── triple-c-task-runner # Cron entry point; permission mode → flags → `claude -p`
│ ├── triple-c-sso-refresh # AWS SSO session refresh helper
│ └── mission-control/ # Bundled Flight Control methodology (skills, docs, templates)
├── stt-container/ # Speech-to-text image
│ ├── Dockerfile # Faster Whisper (Python 3.11 + FastAPI)
│ └── server.py # POST /transcribe endpoint
├── .gitea/
│ └── workflows/
│ ├── build-app.yml # Build Tauri app (Linux/macOS/Windows)
│ ├── build-app-preview.yml # Preview builds
│ ├── build.yml # Build container image (multi-arch)
│ ├── build-stt.yml # Build the STT image
│ ├── sync-release.yml # Mirror releases to GitHub
── backfill-releases.yml # Bulk copy releases to GitHub
── backfill-releases.yml # Bulk copy releases to GitHub
│ └── cleanup-releases.yml # Prune old releases
└── app/ # Tauri v2 desktop application
├── package.json # React, xterm.js, zustand, tailwindcss
├── vite.config.ts # Vite bundler config
├── vitest.config.ts # Vitest (jsdom) config
├── index.html # HTML entry point
├── src/ # React frontend
│ ├── main.tsx # React DOM root
│ ├── App.tsx # Top-level layout
│ ├── index.css # CSS variables, dark theme, scrollbars
│ ├── App.tsx # Top-level layout + welcome screen
│ ├── index.css # CSS variables, dark theme, focus ring, scrollbars
│ ├── store/
│ │ └── appState.ts # Zustand store (projects, sessions, UI)
│ │ └── appState.ts # Zustand store (projects, sessions, tab strip, toasts)
│ ├── hooks/
│ │ ├── useClaudeAuth.ts # Shared token status + acquisition
│ │ ├── useContainerProgress.ts # container-progress events → inline progress
│ │ ├── useDocker.ts # Docker status, image build/pull
│ │ ├── useFileManager.ts # File manager operations
│ │ ├── useFileManager.ts # File browser operations
│ │ ├── useInstallHelper.ts # Guided Docker installation
│ │ ├── useKeyboardShortcuts.ts # Ctrl+T / Ctrl+Shift+W / Ctrl+Tab / Ctrl+1..9
│ │ ├── useProjectActions.ts # Start/stop/reset/backup, open terminals
│ │ ├── useProjects.ts # Project CRUD operations
│ │ ├── useSaveState.ts # Saved / Saving / Failed indicator state
│ │ ├── useSettings.ts # App settings
│ │ ├── useSTT.ts # Speech-to-text recording and container control
│ │ ├── useTerminal.ts # Terminal I/O, resize, session events
│ │ ├── useUpdates.ts # App update checking
│ │ └── useVoice.ts # Voice mode audio capture
│ ├── lib/
│ │ ├── types.ts # TypeScript interfaces matching Rust models
│ │ ├── tauri-commands.ts # Typed invoke() wrappers
│ │ ├── urlDetector.ts # Long-URL reassembly for OAuth flows
│ │ ├── wav.ts # WAV encoding for STT
│ │ └── constants.ts # App-wide constants
│ └── components/
│ ├── layout/ # Sidebar, TopBar, StatusBar
│ ├── projects/ # ProjectCard, ProjectList, AddProjectDialog,
│ │ # FileManagerModal, ContainerProgressModal, modals
│ ├── DockerInstallDialog.tsx # First-run Docker setup
│ ├── layout/ # TopBar, MainTabs (the unified tab strip),
│ │ # Sidebar, StatusBar, HelpDialog
│ ├── projects/
│ │ ├── home/ # Project Home — the main-area project view
│ │ │ ├── ProjectHome.tsx # Header, actions, overflow menu, tab strip
│ │ │ ├── OverviewTab.tsx # Permission mode, summary, recent activity
│ │ │ ├── SessionsTab.tsx # Past Claude sessions + Resume
│ │ │ ├── AutomationTab.tsx # Scheduler tasks + notifications
│ │ │ ├── ConfigTab.tsx # Config section host
│ │ │ ├── FilesTab.tsx # In-container file browser
│ │ │ ├── CapabilityTiles.tsx # Read-only capability counts
│ │ │ ├── format.ts # Age / size / uptime formatting
│ │ │ └── config/ # WorkspaceSection, ModelSection,
│ │ │ # AccessSection, RuntimeSection
│ │ ├── ProjectRow.tsx # Select-only sidebar row
│ │ ├── ProjectList.tsx # Sidebar project list
│ │ ├── AddProjectDialog.tsx # New-project dialog
│ │ ├── PermissionModeControl.tsx # Plan/Default/Accept Edits/Bypass
│ │ ├── ConfirmRemoveModal.tsx # Project removal confirmation
│ │ └── *Editor.tsx / *Modal.tsx # EnvVars, PortMappings,
│ │ # ClaudeInstructions, ClaudeCodeSettings —
│ │ # editors reused by Project Home
│ ├── settings/ # SettingsPanel, DockerSettings, AwsSettings,
│ │ # WebTerminalSettings, UpdateDialog
└── terminal/ # TerminalView (xterm.js), TerminalTabs, UrlToast
│ │ # OllamaSettings, OpenAiCompatibleSettings,
│ # SharedAuthSettings, ClaudeAuthModal,
│ │ # WebTerminalSettings, SttSettings,
│ │ # MicrophoneSettings, UpdateDialog, ImageUpdateDialog
│ ├── terminal/ # TerminalView (xterm.js), TerminalContextMenu,
│ │ # SttButton, UrlToast, trimSelection
│ └── ui/ # Shared primitives: Modal, Button, Toggle, Field,
│ # SegmentedControl, StatusIndicator, SaveIndicator,
│ # OverflowMenu, ToastHost, Tooltip, AccordionSection
└── src-tauri/ # Rust backend
├── Cargo.toml # Rust dependencies
├── tauri.conf.json # Tauri app configuration
├── build.rs # Tauri build script
├── capabilities/
│ └── default.json # Tauri v2 permission grants
│ └── default.json # Tauri v2 plugin permission grants
└── src/
├── lib.rs # App builder, plugin + command registration
├── main.rs # Entry point
├── logging.rs # Log configuration
├── commands/ # Tauri command handlers
│ ├── docker_commands.rs # Docker status, image ops
│ ├── file_commands.rs # File manager (list/download/upload)
│ ├── project_commands.rs # Start/stop/rebuild containers
│ ├── settings_commands.rs # Settings CRUD
│ ├── terminal_commands.rs # Terminal I/O, resize
│ ├── update_commands.rs # App update checking
│ ├── auth_bridge_commands.rs # Enable/status for the loopback bridge
│ ├── auth_token_commands.rs # claude setup-token flow, redaction, keychain
│ ├── aws_commands.rs # AWS profile/region discovery
│ ├── docker_commands.rs # Docker status, image ops
│ ├── file_commands.rs # File browser (list/download/upload)
│ ├── help_commands.rs # Serves HOW-TO-USE.md to the Help dialog
│ ├── inspect_commands.rs # Sessions, capabilities, scheduler tasks
│ ├── install_helper_commands.rs # Guided Docker installation
│ ├── project_commands.rs # Start/stop/rebuild/backup containers
│ ├── settings_commands.rs # Settings CRUD
│ ├── stt_commands.rs # STT start/stop/transcribe
│ ├── terminal_commands.rs # Terminal I/O, resize
│ ├── update_commands.rs # App update checking
│ └── web_terminal_commands.rs # Web terminal start/stop/status
├── web_terminal/ # Remote terminal access
├── auth_bridge/ # Host-side loopback callback bridge
│ ├── mod.rs # Per-project poller, status, lifecycle
│ ├── proc_net.rs # /proc/net/tcp{,6} parsing, loopback filtering
│ └── tunnel.rs # Host loopback bind + socat tunnel over the Docker API
├── web_terminal/ # Remote terminal access
│ ├── mod.rs # Module root
│ ├── server.rs # Axum HTTP+WS server lifecycle
│ ├── ws_handler.rs # WebSocket connection handler
│ └── terminal.html # Embedded xterm.js web UI
├── install_helper/ # Docker installation assistance
│ ├── mod.rs # Install orchestration
│ └── platform.rs # Per-OS install strategies
├── docker/ # Docker API layer
│ ├── client.rs # bollard singleton connection
│ ├── container.rs # Create, start, stop, remove, fingerprinting
├── exec.rs # PTY exec sessions with bidirectional streaming
── image.rs # Build from Dockerfile, pull from registry
│ ├── container.rs # Create/start/stop/remove, labels, recreation checks,
│ # remove_project_volumes, snapshot commit
── exec.rs # create_attached_exec() — the single attached-exec path
│ ├── image.rs # Build from Dockerfile, pull from registry
│ ├── stt.rs # Speech-to-text container lifecycle
│ └── legacy_cleanup.rs # Migration shim for the removed MCP feature
├── models/ # Data structures
│ ├── project.rs # Project, Backend, BedrockConfig
│ ├── app_settings.rs # Global settings (image source, AWS, etc.)
│ ├── project.rs # Project, Backend, PermissionMode, BedrockConfig, …
│ ├── app_settings.rs # Global settings (image source, AWS, STT, web terminal)
│ ├── container_config.rs # Image name resolution
│ └── update_info.rs # Update metadata
└── storage/ # Persistence
├── projects_store.rs # JSON file with atomic writes
├── settings_store.rs # App settings (Tauri plugin-store)
└── secure.rs # OS keychain via keyring
└── secure.rs # OS keychain via keyring (secrets, shared token)
```
---
@@ -339,6 +530,11 @@ triple-c/
| `tar` | 0.4 | In-memory tar archives for Docker build context |
| `dirs` | 6.x | Cross-platform app data directory paths |
| `serde` / `serde_json` | 1.x | Serialization for IPC and persistence |
| `log` / `fern` | 0.4 / 0.7 | Date-based file logging |
| `include_dir` | 0.7 | Embeds the container build context in the binary |
| `reqwest` | 0.12 | HTTPS (rustls) for update checks, help content, STT uploads |
| `iana-time-zone` | 0.1 | Host timezone detection for container `TZ` |
| `sha2` | 0.10 | Settings fingerprints |
| `axum` | 0.8 | HTTP+WebSocket server for web terminal |
| `tower-http` | 0.6 | CORS middleware for web terminal |
| `base64` | 0.22 | Terminal data encoding over WebSocket |
@@ -361,6 +557,8 @@ triple-c/
| `zustand` | 5.x | Lightweight state management |
| `tailwindcss` | 4.x | Utility-first CSS framework |
| `vite` | 6.x | Frontend build tool and dev server |
| `vitest` | 4.x | Test runner (jsdom environment) |
| `@testing-library/react` | 16.x | Component tests |
### Container Image
@@ -0,0 +1,57 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, act } from "@testing-library/react";
import ConfirmResetModal from "./ConfirmResetModal";
/** Modal focuses via rAF so the panel is laid out first; jsdom needs a flush. */
async function flushFocus() {
await act(async () => {
vi.advanceTimersByTime(20);
});
}
describe("ConfirmResetModal", () => {
beforeEach(() => {
vi.useFakeTimers({ toFake: ["requestAnimationFrame", "setTimeout"] });
});
afterEach(() => {
vi.useRealTimers();
});
async function renderModal() {
const onConfirm = vi.fn();
const onCancel = vi.fn();
render(
<ConfirmResetModal
projectName="api-server"
onConfirm={onConfirm}
onCancel={onCancel}
/>,
);
await flushFocus();
return { onConfirm, onCancel };
}
it("names what will be lost rather than just asking to confirm", async () => {
await renderModal();
// The whole point of the gate: Reset deletes the volumes, and the two
// losses users do not expect are the login and the session transcripts.
expect(screen.getByText(/sign in again/i)).toBeInTheDocument();
expect(screen.getByText(/session transcript/i)).toBeInTheDocument();
// And it must say what is safe, or the warning reads as "you lose everything".
expect(screen.getByText(/mounted project folders/i)).toBeInTheDocument();
});
it("does not reset until confirmed", async () => {
const { onConfirm, onCancel } = await renderModal();
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
expect(onCancel).toHaveBeenCalledTimes(1);
expect(onConfirm).not.toHaveBeenCalled();
});
it("resets on confirm", async () => {
const { onConfirm } = await renderModal();
fireEvent.click(screen.getByRole("button", { name: "Reset container" }));
expect(onConfirm).toHaveBeenCalledTimes(1);
});
});
@@ -0,0 +1,66 @@
import Modal from "../ui/Modal";
import Button from "../ui/Button";
interface Props {
projectName: string;
onConfirm: () => void;
onCancel: () => void;
}
/**
* Reset is destructive in a way its name does not advertise.
*
* `rebuild_project_container` calls `remove_project_volumes`, which deletes
* both `triple-c-home-{id}` and `triple-c-claude-config-{id}` — so the OAuth
* login, any skills or agents installed in the container, and every session
* transcript go with them. That is intentional (Reset exists to get back to a
* clean base image), but it is not recoverable, so it gets the same
* confirmation gate as Remove.
*/
export default function ConfirmResetModal({ projectName, onConfirm, onCancel }: Props) {
return (
<Modal
title="Reset container"
onClose={onCancel}
widthClassName="w-[28rem]"
footer={
<>
<Button size="md" variant="ghost" onClick={onCancel}>
Cancel
</Button>
<Button
size="md"
onClick={onConfirm}
className="bg-[var(--error-emphasis)] text-white border border-transparent hover:opacity-90"
>
Reset container
</Button>
</>
}
>
<div className="space-y-2.5 text-[13px] text-[var(--text-secondary)]">
<p>
Rebuild{" "}
<strong className="text-[var(--text-primary)]">{projectName}</strong>&rsquo;s
container from the clean base image.
</p>
<p>
This deletes the container&rsquo;s volumes, which means you will lose:
</p>
<ul className="list-disc pl-5 space-y-1">
<li>
your <code className="font-mono">claude login</code> &mdash; you will need to
sign in again
</li>
<li>any skills, agents or plugins installed inside the container</li>
<li>every saved session transcript, so past sessions cannot be resumed</li>
<li>anything installed with <code className="font-mono">apt</code>, <code className="font-mono">pip</code> or <code className="font-mono">npm</code></li>
</ul>
<p>
Your mounted project folders are on the host and are{" "}
<strong className="text-[var(--text-primary)]">not</strong> affected.
</p>
</div>
</Modal>
);
}
@@ -8,6 +8,7 @@ import { ProjectStatusIndicator } from "../../ui/StatusIndicator";
import Button from "../../ui/Button";
import OverflowMenu from "../../ui/OverflowMenu";
import ConfirmRemoveModal from "../ConfirmRemoveModal";
import ConfirmResetModal from "../ConfirmResetModal";
import OverviewTab from "./OverviewTab";
import SessionsTab from "./SessionsTab";
import AutomationTab from "./AutomationTab";
@@ -39,6 +40,7 @@ export default function ProjectHome({ projectId, active }: Props) {
const project = projects.find((p) => p.id === projectId);
const [tab, setTab] = useState<ProjectHomeTabId>("overview");
const [confirmRemove, setConfirmRemove] = useState(false);
const [confirmReset, setConfirmReset] = useState(false);
const { runningSince, progress } = useAppState(
useShallow((s) => ({
runningSince: s.runningSince[projectId],
@@ -144,9 +146,10 @@ export default function ProjectHome({ projectId, active }: Props) {
disabled: actions.backingUp || !project.container_id,
},
{
label: "Reset container",
onSelect: actions.handleReset,
label: "Reset container",
onSelect: () => setConfirmReset(true),
disabled: !isStopped || actions.busy,
danger: true,
},
{
label: "Remove project…",
@@ -205,6 +208,16 @@ export default function ProjectHome({ projectId, active }: Props) {
{tab === "files" && <FilesTab project={project} />}
</div>
{confirmReset && (
<ConfirmResetModal
projectName={project.name}
onCancel={() => setConfirmReset(false)}
onConfirm={() => {
setConfirmReset(false);
actions.handleReset();
}}
/>
)}
{confirmRemove && (
<ConfirmRemoveModal
projectName={project.name}