Metadata-Version: 2.4
Name: trusty-cage
Version: 0.13.0
Summary: Isolated Docker-based development environments for AI coding agents
Project-URL: Homepage, https://github.com/areese801/trusty-cage
Project-URL: Repository, https://github.com/areese801/trusty-cage
Project-URL: Issues, https://github.com/areese801/trusty-cage/issues
Author: Adam Reese
License-Expression: MIT
License-File: LICENSE
Keywords: agents,ai,claude,containers,development,docker,isolation
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development
Requires-Python: >=3.11
Requires-Dist: pathspec>=0.12
Requires-Dist: python-dotenv>=1.0
Requires-Dist: typer[all]>=0.12
Provides-Extra: dev
Requires-Dist: build; extra == 'dev'
Requires-Dist: pytest-mock>=3.12; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Requires-Dist: twine; extra == 'dev'
Description-Content-Type: text/markdown

# trusty-cage

Isolated Docker-based development environments for AI coding agents. Run Claude Code (or any agent) with full autonomy inside a disposable container — no risk to your host machine, no credentials exposed, no accidental pushes.

## Installation

```bash
pip install trusty-cage
```

Or with [pipx](https://pipx.pypa.io/) for isolated CLI installs:

```bash
pipx install trusty-cage
```

`tc` is available as a shorthand for `trusty-cage` (e.g. `tc create ...`, `tc attach ...`).

## Quick Start

```bash
# One-time setup: create config directory and default .env file
trusty-cage init

# Create an environment from any git repo
trusty-cage create https://github.com/octocat/Hello-World

# You're now inside a tmux session (prefix: Ctrl-a) with:
#   Left pane (60%)  — Neovim at the project root
#   Top-right pane   — Claude Code running with --dangerously-skip-permissions
#   Bottom-right pane — plain shell
#
# If you configured TRUSTY_CAGE_DOTFILES_REPO, your shell config, aliases,
# tmux settings, and Neovim config are already applied — the container
# should feel like your own machine.

# Switch panes with Ctrl-a <arrow>, detach with Ctrl-a d

# When done, export your work back to the host:
trusty-cage export hello-world

# Review and push from the host clone:
cd ~/.trusty-cage/envs/hello-world/repo/
git diff
git add -A && git commit -m "work from trusty-cage"
git push

# Or copy into an existing clone (don't forget the trailing /):
cd ~/projects/hello-world
cp -R ~/.trusty-cage/envs/hello-world/repo/ .
```

## Demo

Here's a real workflow using trusty-cage to build an [Obsidian plugin](https://github.com/areese801/obsidian-todoist) from scratch.

### 1. Give instructions inside the cage

![Inside the cage — giving Claude Code instructions](https://raw.githubusercontent.com/areese801/trusty-cage/main/.images/trusty-cage-inside-instructions.png)

The terminal title bar shows the `tc create` command that built this environment. Inside, Claude Code runs with full autonomy (`bypass permissions on`) in a tmux session alongside Neovim. The container's git repo has **no remotes** — the agent can commit locally but has no way to push anywhere.

### 2. Let the agent work autonomously

![Claude Code working autonomously](https://raw.githubusercontent.com/areese801/trusty-cage/main/.images/trusty-cage-inside-auto-work.png)

Claude Code explored a reference project, designed an architecture, wrote the full plugin (TypeScript, settings UI, API client, parser), and committed everything — all without any human intervention. The agent had full control inside the container: installing packages, creating files, running builds. If anything went wrong, the host machine would be completely unaffected.

### 3. Export and review on the host

![Exporting work back to the host](https://raw.githubusercontent.com/areese801/trusty-cage/main/.images/trusty-cage-outside-export-work.png)

Back on the host, `tc export` copies the container's work into the host clone at `~/.trusty-cage/envs/<name>/repo/`. From there, you review the diff, commit, and push — the human stays in the loop for all git operations that touch a remote.

**To work from the exported repository:**

```bash
cd ~/.trusty-cage/envs/obsidian-todoist/repo/
git diff
git add -A && git commit -m "work from trusty-cage"
git push
```

**To copy exported code into your own cloned repository:**

If you already have the repo cloned elsewhere (e.g. `~/projects/personal/obsidian-todoist`), you can copy the exported files into it instead. Make sure you're `cd`'d into your clone first, and **don't forget the trailing `/`** on the source path — on macOS (BSD `cp`), the trailing `/` copies the _contents_ of the directory rather than the directory itself:

```bash
cd ~/projects/personal/obsidian-todoist
cp -R ~/.trusty-cage/envs/obsidian-todoist/repo/ .
git status   # review what changed
git diff
git add -A && git commit -m "work from trusty-cage"
git push
```

**Linux note:** GNU `cp` ignores the trailing `/` and always copies the directory itself. On Linux, use `cp -RT` or `rsync -a` instead:

```bash
cp -RT ~/.trusty-cage/envs/obsidian-todoist/repo .
rsync -a ~/.trusty-cage/envs/obsidian-todoist/repo/ .
```

> The `Permission denied` errors on `.git/objects/pack` files are expected and harmless — your host `.git/` is preserved and those locked pack files don't need to be overwritten.

## Example: Hello World

```bash
# Create (environment name is derived as lowercase: "hello-world")
trusty-cage create https://github.com/octocat/Hello-World --no-attach

# Verify
trusty-cage list
docker ps -a | grep isolated-dev

# Attach — drops you into tmux inside the container
trusty-cage attach hello-world

# Inside the container:
#   Ctrl-a <arrow>    — switch tmux panes
#   git remote -v     — empty (no remotes, by design)
#   curl example.com  — works (outbound web allowed)
#   Ctrl-a d          — detach

# Export work back to host
trusty-cage export hello-world

# Clean up
trusty-cage destroy hello-world
```

## Example: Local Directory

No remote repo? Use `--dir` to create a cage from any local directory:

```bash
# Create from current directory
trusty-cage create --dir . --name my-project --no-attach

# Or from an explicit path
trusty-cage create --dir ~/projects/my-project --no-attach

# Everything else works the same
trusty-cage attach my-project
trusty-cage export my-project
trusty-cage destroy my-project
```

## Additional Directories

Ship extra local directories into a cage alongside the main project. Each additional directory gets its own Docker volume and host clone, with full export/sync/diff support.

```bash
# Include at create time
tc create --dir ~/projects/main-app --add-dir ~/projects/shared-lib --name my-cage --no-attach

# Or add to an existing cage (recreates the container with a new volume mount)
tc add-dir my-cage ~/projects/shared-lib

# Additional dirs appear in the container's home directory
# /home/trustycage/project       ← main project (always)
# /home/trustycage/shared-lib    ← additional dir

# Export, diff, and sync work with --dir and --all
tc diff my-cage --dir shared-lib           # diff one additional dir
tc diff my-cage --all                      # diff main project + all additional dirs
tc export my-cage --dir shared-lib -y      # export one additional dir
tc export my-cage --all -y                 # export everything
tc sync my-cage --dir shared-lib -y        # push host changes into one dir

# Remove a directory (removes volume and host clone)
tc remove-dir my-cage shared-lib -y

# Destroy cleans up all volumes (main + additional dirs)
tc destroy my-cage -y
```

Directory names are derived from the path basename (lowercased, sanitized). Override with `--name`:

```bash
tc add-dir my-cage ~/projects/My.Library --name mylib
```

The name `project` is reserved for the main project directory.

## Commands

| Command                                                                                               | Description                                                                    |
| ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `trusty-cage --version`                                                                               | Show version and exit                                                          |
| `trusty-cage init [--force]`                                                                          | Create config directory and default `.env` file                                |
| `trusty-cage create <url> [--name] [--no-attach] [--auth-mode] [--dockerfile] [--add-dir]`            | Create a new environment from a git repo                                       |
| `trusty-cage create --dir <path> [--name] [--no-attach] [--auth-mode] [--dockerfile] [--add-dir]`     | Create from a local directory (no remote required)                             |
| `trusty-cage attach <name>`                                                                           | Attach to an existing environment                                              |
| `trusty-cage stop <name>`                                                                             | Stop a container (preserves work)                                              |
| `trusty-cage list [--json]`                                                                           | List all environments with status and additional dirs                          |
| `trusty-cage exists <name>`                                                                           | Check if an environment exists (exit code 0/1)                                 |
| `trusty-cage add-dir <name> <path> [--name]`                                                          | Add a local directory to an existing cage                                      |
| `trusty-cage remove-dir <name> <dir-name> [-y/--yes]`                                                 | Remove an additional directory from a cage                                     |
| `trusty-cage export <name> [-y/--yes] [--output-dir] [--delete] [--protect] [--dir] [--all] [--stats] [--include-cache] [--no-tidy]` | Copy work back to host clone (safe default: no `--delete`) |
| `trusty-cage diff <name> [--full] [--output-dir] [--dir] [--all] [--stats] [--delete] [--include-cache] [--no-tidy]` | Preview what `export` would change (dry run) |
| `trusty-cage sync <name> [--files] [-y/--yes] [--dir] [--all] [--include-cache]`                      | Push host files into a cage (inverse of export)                                |
| `trusty-cage patch <name> [--base] [--output-dir] [--stdout]`                                         | Emit `git format-patch` for cage commits ahead of base (alternative to export) |
| `trusty-cage tidy <name> [--paths] [--dry-run]`                                                       | Remove cache directories (`.mypy_cache`, etc.) from the cage working tree      |
| `trusty-cage destroy <name> [-y/--yes] [--keep-host-clone]`                                           | Remove container, volumes, and host clone (pass `--keep-host-clone` to retain) |
| `trusty-cage rebuild-image [--dockerfile]`                                                            | Force rebuild the Docker image                                                 |
| `trusty-cage auth <name> [--login]`                                                                   | Refresh or verify credentials for an environment                               |
| `trusty-cage launch <name> -p/--prompt\|--prompt-file\|--test [--background] [--no-inject-messaging]` | Launch Claude Code inside a cage (messaging instructions injected by default)  |
| `trusty-cage logs [name] [-f] [-r/--raw] [-n/--lines N]`                                              | Stream inner Claude's reasoning (pretty-printed by default)                    |
| `trusty-cage outbox <name> [-a/--all] [--json] [--poll] [--timeout] [--interval] [--max-interval] [--no-diagnose]` | Read messages from a cage's outbox (adaptive polling, auto-diagnoses on timeout) |
| `trusty-cage inbox <name> <type> <payload_json>`                                                      | Send a message to a cage's inbox                                               |
| `trusty-cage diagnose <name> [--json]`                                                                | Diagnostic sweep: inner process state, outbox, git, stream tail, suggestion    |
| `trusty-cage salvage <name> [-y/--yes] [--output-dir]`                                                | Rescue work from a cage that didn't reach task_complete (diagnose + export)    |

## Export, Diff, and Sync

### Safe Export (default)

By default, `tc export` copies cage files onto the host **without** deleting host-only files. This prevents accidental removal of `.gitignore`, `venv/`, `.env`, and other files that exist only on the host.

```bash
tc export myenv -y                     # safe: adds/updates files, never deletes
tc export myenv -y --delete            # opt-in: mirror cage exactly (deletes host-only files)
tc export myenv -y --protect "*.md"    # exclude additional patterns from sync
tc export myenv -y --dir shared-lib    # export a specific additional dir
tc export myenv -y --all               # export main project + all additional dirs
```

Files matching patterns in your `.gitignore` and `.cageprotect` are always excluded from both overwrite and deletion, even with `--delete`.

### `.cageprotect` file

Create a `.cageprotect` file in your project root to protect host-only files during export. Same format as `.gitignore` — one pattern per line, `#` for comments:

```
# Host-only config (never touched by tc export)
CLAUDE.md
.env.production
secrets/

# Protect host .gitignore if you don't want the cage version overwriting it
.gitignore
```

**How it works:** `tc export` rsyncs files from the cage to the host. Patterns in `.gitignore` (read from the host target) are excluded from both overwrite and deletion. `.cageprotect` adds additional patterns that you want protected beyond what `.gitignore` covers.

**Hardcoded exclusions:** `.git/`, `.cageprotect`, `venv/`, and `.venv/` are always excluded, regardless of config.

**Cache excludes (default):** `__pycache__/`, `*.py[cod]`, `.pytest_cache/`, `.ruff_cache/`, `.mypy_cache/`, `.DS_Store`, and `node_modules/` are excluded too. Pass `--include-cache` on `tc export` / `tc diff` / `tc sync` to transfer them anyway.

**Auto-tidy before export / diff:** `tc diff` and `tc export` also run `tc tidy` against the cage before their main work — an extra belt-and-suspenders pass that physically removes the cache directories from the cage tree so they can't leak through. Silent unless matches exist. Pass `--no-tidy` to skip, or `--include-cache` (which also skips tidy since you've opted into keeping caches). You can also run `tc tidy <env>` manually at any time, optionally with `--dry-run` to preview or `--paths <name>` to override the default cache-dir list.

**About `.gitignore`:** The cage's `.gitignore` file itself **does** transfer to the host during export — if an agent adds a new pattern to `.gitignore` inside the cage, that change reaches the host. If you want to protect the host's `.gitignore` from being overwritten, list it in `.cageprotect`.

**`.gitignore.pre-export` backup:** When the cage's `.gitignore` differs from the host's, `tc export` writes the host's version to `.gitignore.pre-export` alongside it and prints a diff. This way you never silently lose a hand-tuned host `.gitignore` to a cage-side change — worst case you `mv .gitignore.pre-export .gitignore` to restore.

### Preview with `tc diff`

See what `tc export` would change before running it:

```bash
tc diff myenv                          # summary table (added/modified/deleted)
tc diff myenv --full                   # full unified diffs
tc diff myenv --output-dir .           # compare against a specific directory
tc diff myenv --dir shared-lib         # diff a specific additional dir
tc diff myenv --all                    # diff main project + all additional dirs
```

### Code Statistics

Pass `--stats` to `tc export` or `tc diff` to see a per-language breakdown of lines added, removed, and modified:

```bash
tc diff myenv --stats
tc export myenv -y --stats
```

Output looks like:

```
           Code Statistics
┏━━━━━━━━━━━━┳━━━━━━━┳━━━━━━┳━━━━━━━━━┳━━━━━━━━━━┓
┃ Language   ┃ Files ┃ Added┃ Removed ┃ Modified ┃
┡━━━━━━━━━━━━╇━━━━━━━╇━━━━━━╇━━━━━━━━━╇━━━━━━━━━━┩
│ Python     │     3 │ +142 │      -8 │      ~21 │
│ Markdown   │     1 │  +12 │      -0 │       ~0 │
├────────────┼───────┼──────┼─────────┼──────────┤
│ Total      │     4 │ +154 │      -8 │      ~21 │
└────────────┴───────┴──────┴─────────┴──────────┘
(via cloc)
```

If [cloc](https://github.com/AlDanial/cloc) is installed on the host, stats are language-aware with a full added / removed / modified breakdown. Without cloc, trusty-cage falls back to a pure-Python line counter (added / removed only, detected by file extension). No extra dependencies either way.

**Gitignored files are not counted.** Stats apply the union of both sides' `.gitignore` rules plus a default cache-pattern set (`.mypy_cache/`, `.pytest_cache/`, `.ruff_cache/`, `__pycache__/`, `.DS_Store`, `node_modules/`) before counting, so transient build noise doesn't inflate the numbers. Pass `--include-cache` to get raw numbers that count everything.

### Export via git patches (`tc patch`)

If the cage's deliverable is one or more clean commits (typical when the inner agent was told to commit its work), `tc patch` is an alternative to `tc export` that ships **only** the commits — no rsync of the working tree, no risk of dragging along cache dirs or `pip install` / `uv sync` side effects:

```bash
# Default: writes one patch file per commit ahead of `main`
tc patch myenv
git am ./.trusty-cage-patches/myenv/*.patch

# Or stream a single combined patch to stdout
tc patch myenv --stdout | git am

# Against a non-`main` base
tc patch myenv --base feature-branch

# Custom output directory
tc patch myenv --output-dir /tmp/cage-patches/
```

Use `tc export` when the cage has uncommitted / work-in-progress changes you want to stage yourself on the host. Use `tc patch` when the deliverable is already committed in the cage and you want only the commit contents — nothing else.

### Push fixes with `tc sync`

After exporting and fixing something on the host, push changes back into a running cage:

```bash
tc sync myenv -y                       # sync all host files into cage
tc sync myenv -y --files "main.py"     # sync specific files only
tc sync myenv -y --dir shared-lib      # sync a specific additional dir
tc sync myenv -y --all                 # sync main project + all additional dirs
```

### Typical workflow

```bash
tc diff myenv                          # preview changes
tc export myenv -y                     # export (safe default)
# ... fix something on the host ...
tc sync myenv -y --files "fixed.py"    # push fix back into cage
tc launch myenv --prompt "Continue"    # resume work in cage
```

## Configuration

Configuration is resolved in order: CLI flags > environment variables > `~/.trusty-cage/.env` > defaults.

| Variable                        | Default   | Description                                                                               |
| ------------------------------- | --------- | ----------------------------------------------------------------------------------------- |
| `TRUSTY_CAGE_DOTFILES_REPO`     | _(empty)_ | HTTPS URL of dotfiles repo to clone into containers                                       |
| `TRUSTY_CAGE_PYTHON_VERSION`    | `3.12`    | Python version installed via pyenv                                                        |
| `TRUSTY_CAGE_DEFAULT_SHELL`     | `zsh`     | Default shell inside the container                                                        |
| `TRUSTY_CAGE_DEFAULT_AUTH_MODE` | `api_key` | Auth mode: `api_key` or `subscription`                                                    |
| `TRUSTY_CAGE_TMUX_PREFIX`       | `C-a`     | tmux prefix key inside containers (default `Ctrl-a` to avoid conflict with host `Ctrl-b`) |
| `TRUSTY_CAGE_AUTO_REBUILD`      | `true`    | Auto-rebuild the Docker image when the Dockerfile has changed; set `false` to only warn   |
| `ANTHROPIC_API_KEY`             | _(none)_  | API key for Claude Code (required for `api_key` auth mode)                                |

Run `trusty-cage init` to create `~/.trusty-cage/.env` with a commented template you can customize.

## Dotfiles

If you set `TRUSTY_CAGE_DOTFILES_REPO`, your dotfiles are automatically applied to every new container at `create` time. The repo is cloned on the host, `.git/` is stripped, and the files are copied into the container's home directory. If an install script is found (`install.sh`, `setup.sh`, `bootstrap.sh`, etc.), it runs automatically. GNU Stow layouts are detected and handled (files are copied from `common/` if present).

This means your shell config, tmux settings, Neovim config, aliases, and other personalizations carry over — the container feels like your own machine.

**Without dotfiles**, the container ships with sensible defaults: oh-my-zsh (robbyrussell theme), LazyVim starter config, pyenv on PATH, `uv` (Astral's Python package manager) preinstalled, and `vim`/`vi` aliased to `nvim`. Everything works out of the box, just without your personal customizations.

## Authentication

Chosen at `create` time via `--auth-mode`:

- **api_key** — Reads `ANTHROPIC_API_KEY` from your host shell at attach/launch time. Injected via `docker exec -e`, never written to disk. Best for API billing users.
- **subscription** — Copies `~/.claude/`, `~/.claude.json`, and OAuth tokens from macOS Keychain into the container at create time. Persists in the volume. Best for Claude Pro/Max subscribers — no API key needed.

```bash
# Create with API key auth (default)
tc create https://github.com/user/repo
# Requires ANTHROPIC_API_KEY to be set in your shell

# Create with subscription auth (Claude Pro/Max)
tc create https://github.com/user/repo --auth-mode subscription
# Automatically extracts OAuth tokens from macOS Keychain

# Refresh credentials at any time
tc auth myenv

# If subscription tokens have expired, re-login interactively
tc auth myenv --login
```

On macOS, subscription mode extracts OAuth tokens from the system Keychain (`Claude Code-credentials`) and writes them as `~/.claude/.credentials.json` inside the container. This bridges macOS Keychain storage with Linux's file-based fallback — no manual `/login` step needed.

## Custom Dockerfile

By default, trusty-cage uses a built-in Dockerfile (Ubuntu 24.04 with Python, Node.js, Neovim, tmux, Claude Code). You can replace it entirely with your own:

```bash
# Via CLI flag (highest priority)
tc create https://github.com/user/repo --dockerfile /path/to/Dockerfile

# Via convention path (used if no flag is passed)
# Place your Dockerfile at ~/.trusty-cage/Dockerfile

# Rebuild the image with a custom Dockerfile
tc rebuild-image --dockerfile /path/to/Dockerfile
```

Custom Dockerfiles fully replace the default — you are responsible for including the `trustycage` user (UID 1000), required tools, and any security constraints your workflow requires.

### Extending the Base Image

If you only need to add packages or tools on top of the default image, use Docker's `FROM` inheritance instead of replacing the entire Dockerfile:

```dockerfile
FROM trusty-cage:latest

# Add system packages
RUN apt-get update && apt-get install -y postgresql-client redis-tools

# Add Python packages
RUN pip install pandas requests

# Add project-specific tooling
COPY my-linter.conf /home/trustycage/.config/
```

Build the base image first (happens automatically on `tc create`), then pass your extending Dockerfile:

```bash
tc create https://github.com/user/repo --dockerfile ./Dockerfile.custom
```

This preserves the base setup (non-root user, tmux, Neovim, Claude Code, network policy) while letting you layer on project-specific dependencies.

## Orchestration

trusty-cage supports two modes of use:

1. **Interactive** — `tc attach` drops you into a tmux session with Claude Code, Neovim, and a shell. You prompt Claude directly and watch it work.
2. **Headless** — `tc launch` runs Claude non-interactively with a prompt. An outer Claude (or script) orchestrates the inner Claude, monitors progress, and exports results.

### Headless Workflow

```bash
# Create a cage (no interactive attach)
tc create https://github.com/user/repo --name myproject --auth-mode subscription --no-attach

# Verify Claude can start (pre-flight check)
tc launch myproject --test

# Send a task
tc launch myproject --prompt "Implement feature X" --background

# Watch the inner Claude's reasoning in real-time (from the host)
tc logs myproject -f

# Or get raw stream-json for programmatic consumption
tc logs myproject -f --raw

# For long prompts, use a file
tc launch myproject --prompt-file /path/to/prompt.txt --background

# Poll for completion (blocks until task_complete arrives)
tc outbox myproject --poll --timeout 1800 --max-interval 300

# Or read outbox messages manually
tc outbox myproject --all --json

# Send a response to the inner Claude's inbox
tc inbox myproject info_response '{"request_id":"req-001","content":"file contents"}'

# When done, export and overlay onto your working directory
tc export myproject -y --output-dir .

# Clean up
tc destroy myproject -y
```

### Monitoring with `tc logs`

`tc logs` streams the inner Claude's reasoning from outside the cage — no attach needed. Output is pretty-printed by default:

```
INIT session=a48c7ada... model=claude-opus-4-6[1m]
THINKING Simple task - create a weather.py script with temperature conversion.
TOOL Write: /home/trustycage/project/weather.py
RESULT File created successfully at: /home/trustycage/project/weather.py
TOOL Bash: python weather.py
RESULT 0°C = 32.00°F ...
CLAUDE Script created and working.
DONE Script created and working.
     cost=$0.1563 duration=10.5s
```

Use `--raw` for the full stream-json output. Use `-f` / `--follow` to tail in real-time.

### Adaptive polling with `tc outbox --poll`

When polling a long-running cage, `--poll` starts at `--interval` seconds (default 30) but lengthens the interval after three consecutive idle polls — growing by 1.5× per cycle up to `--max-interval` (default 300s). Any new message resets the interval back to the floor, so responsiveness is preserved when the inner agent is actively producing output. Short cages that emit messages frequently never cross the idle threshold and behave exactly like fixed-interval polling.

### Diagnose and Salvage

Two commands for when something goes sideways inside a cage:

- **`tc diagnose <name>`** runs a quick sweep and prints a report: is the inner Claude process alive, zombie, or exited? how many messages are in the outbox and what was the most recent one? what does `git status` look like inside the cage? what are the last few lines of the stream log? Use this when you're not sure whether the cage is working, stuck, or done. Pass `--json` for scripted consumers. Safe to run against stopped or missing cages. `tc outbox --poll` auto-runs diagnose on timeout unless `--no-diagnose` is passed.
- **`tc salvage <name>`** rescues work from a cage that didn't reach `task_complete`. Internally it runs `tc diagnose`, warns on suspicious states (alive inner, stopped container, clean git), and then exports whatever is in the cage into the current directory (or `--output-dir`). Unlike `tc destroy`, salvage intentionally **preserves** the cage so you can poke around — run `tc destroy <name>` yourself once you've got what you need.

### `cage-send` (Inside the Container)

The container image includes `cage-send` at `/usr/local/bin/cage-send` — a helper script that handles message envelope construction (id, timestamp, version) so the inner Claude doesn't have to build JSON by hand:

```bash
# Report progress
cage-send progress_update '{"status":"implementing auth module","detail":"3 of 5 files done"}'

# Request a file from the host
cage-send info_request '{"request_id":"req-001","description":"Need package.json","paths":["package.json"]}'

# Report an error
cage-send error '{"error_type":"missing_dep","message":"need ffmpeg","recoverable":true}'

# Signal completion (REQUIRED as final action)
cage-send task_complete '{"summary":"Implemented feature X","exit_code":0}'
```

Valid types: `task_complete`, `progress_update`, `error`, `info_request`, `going_idle`. The script validates the type and JSON payload before writing.

### Messaging System

The container includes a file-based message bus for structured communication between the inner and outer Claude. Messages are timestamped JSON files in well-known directories:

```
/home/trustycage/.cage/
  outbox/                  # Inner Claude writes here (via cage-send), outer reads (via tc outbox)
  inbox/                   # Outer Claude writes here (via tc inbox), inner reads
  cursor/                  # Tracks read position (Kafka-like offset)
  claude-stream.log        # Stream-json output from tc launch (readable via tc logs)
```

**Message types:**

| Type              | Direction      | Purpose                                          |
| ----------------- | -------------- | ------------------------------------------------ |
| `task_complete`   | inner -> outer | Signal task is done (includes summary)           |
| `progress_update` | inner -> outer | Report what's being worked on                    |
| `info_request`    | inner -> outer | Ask for files/data from the host                 |
| `error`           | inner -> outer | Report a blocker                                 |
| `info_response`   | outer -> inner | Respond to an info_request                       |
| `ack`             | outer -> inner | Acknowledge receipt of a message                 |
| `task_revision`   | outer -> inner | Send revised instructions after review           |
| `going_idle`      | inner -> outer | Inner Claude's polling timed out, session ending |

**Message format:**

```json
{
  "id": "msg-20260326T143000-a1b2",
  "type": "task_complete",
  "timestamp": "2026-03-26T14:30:00.000Z",
  "payload": { "summary": "Implemented feature X", "exit_code": 0 },
  "version": 1
}
```

The messaging system is initialized automatically during `tc create`. The new `task_revision` and `going_idle` message types support an iteration loop — inner Claude polls for revised instructions after completing a task, allowing multiple rounds of feedback without destroying the cage.

### Using the Cage Orchestrator Skill

The easiest way to use trusty-cage in headless mode is through the **cage-orchestrator** skill. Instead of running CLI commands manually, you tell your outer Claude to delegate work to an isolated cage:

> "Spin up a cage and implement feature X in this repo"

The cage-orchestrator skill handles the full lifecycle automatically:

1. **Creates** a cage from the current repo's git remote
2. **Launches** inner Claude with your task description
3. **Monitors** progress via `tc logs` and `tc outbox --poll`
4. **Relays** progress updates and handles information requests from inner Claude
5. **Exports** completed work back to your working directory
6. **Reviews** changes with you (`git diff`)
7. **Iterates** — if you have feedback, it sends revised instructions back to inner Claude (who stays alive and keeps context), and the cycle repeats
8. **Cleans up** when you're done

The inner Claude works with full autonomy inside the container — installing packages, editing files, running tests — while the outer Claude keeps you informed and handles anything that requires host access (git push, file retrieval, approvals).

#### Install the skills

Install the companion plugin by running these commands inside a [Claude Code](https://claude.ai/code) session:

```
/plugin marketplace add areese801/trusty-cage-plugin
/plugin install trusty-cage@trusty-cage-plugin
```

The skills activate automatically when you ask Claude to run something in a cage. See [trusty-cage-plugin](https://github.com/areese801/trusty-cage-plugin) for details.

## Security Model

The container is the blast radius. If an agent does something destructive, your host is unaffected.

**What agents can do inside:**

- Clone/fetch public repos over HTTPS
- Browse the web, read docs, hit public APIs
- Install packages (pip, apt, npm)
- Full read/write access to the project directory

**What agents cannot do:**

- Push to any git remote (no credentials present)
- Use SSH (port 22 blocked)
- Pull Docker images from Docker Hub (blocked)
- Access any host files (no bind mounts)

Protection is enforced by **credential absence**, not network blocking. The container has no SSH keys, no `.netrc`, no `GH_TOKEN`, no git credential helper.

## Requirements

- Docker (Docker Desktop, OrbStack, or Docker Engine)
- Python 3.11+
- Git
- rsync (pre-installed on macOS and most Linux distros; used by `export`)

## Development

```bash
git clone https://github.com/areese801/trusty-cage.git
cd trusty-cage
python -m venv venv
source venv/bin/activate
pip install -e ".[dev]"

# Available make targets
make help
```

## License

MIT
