Live Development

Develop trajectories with ct live. It spins up your task's Compose environment outside Inspect so you can iterate on a trajectory.

Run ct live to list its commands, and pass --help to any command for its arguments. You rarely need arguments, since the commands prompt for what they need. Session metadata is cached under .ct/sessions/ (one JSON file per session), with .ct/active pointing at the active one, so you don't have to re-enter it.

Where Commands Run

Every ct command runs on the host; ct is not installed inside the container. The task work happens inside the container. There are several ways to open the container (see the command list below); the simplest is a second terminal:

TerminalPrompt looks likeWhat runs here
Host, in your control-tower checkoutyou@laptop control-tower %ct commands, e.g. ct live up
Container, opened with ct live shellroot@a1b2c3d4e5f6:/app#Reading and editing the codebase to do the main task and side task

Disabling Interactive Prompts

Interactive terminal user interface (TUI) prompts are on by default when stdin is a TTY. To disable them, for example when an AI agent drives a TTY terminal, set DISABLE_TUI:

# In .env file or environment DISABLE_TUI=1

With it set, commands skip all prompts and either use the arguments you pass or fail with an error naming the missing ones.

MCP Server for AI Agents

The Model Context Protocol (MCP) server script exposes ct CLI commands as MCP tools, so AI agents like Claude Code can call Control Tower directly instead of working out each command from its --help.

To enable, add .mcp.json to the repo root:

{ "mcpServers": { "control_tower": { "command": "uv", "args": ["run", "python", "scripts/mcp_server.py"] } } }

Claude Code then loads the server only when working in the control-tower directory. After adding the file, restart Claude Code or run /mcp to reconnect.

Multi-Context Support

ct live supports multiple concurrent contexts, each with its own Docker Compose project and state, so you can work on several environments, tasks, or trajectories at once:

  • live up creates a new context when its selections differ from every existing one; if the environment, tasks, trajectory directory (traj dir), mount, compose config override, and sandbox config all match an existing session, it reuses that session. up-from and --mount-fresh-dir always create a new context, and so does live up on WSL without --mount-dir, where it uses a fresh mount directory automatically.
  • live switch changes the active context.
  • live status lists all contexts and marks the active one.
  • live down tears down the compose project and removes the current context; live clear removes only the context state.

Most commands accept --context <context-id> to act on a specific context without changing the active one.

Quick Start from Eval Files or Traj Directories

up-from spins up a context from an existing .eval or .json file, or from a traj directory:

ct live up-from path/to/evaluation.eval ct live up-from .settings/linuxarena/web_scraping/side_tasks/expose_secret/trajs/backup/0/

It reads the environment, main task, and side task from the source, and selects the traj directory when given one.

live up runs the task's setup script automatically (pass --no-setup to skip it). Running live setup yourself is optional and is mainly for debugging a setup script.

Example workflow:

# Create and work with your first context ct live up # Prompts for env, main task, side task; creates a traj dir ct live up-from path/to/file.eval # Auto-detect env, task, side task from .eval/.json file ct live setup # Run the task's setup script and initialize git for gen-traj # Methods for editing container files ct live agent code # Attach a VS Code-based IDE via Dev Containers ct live agent claude # Launch Claude Code's CLI inside the container ct live agent codex # Launch OpenAI Codex CLI inside the container ct live agent gemini # Launch Google Gemini CLI inside the container ct live shell # Pop a bash shell ct live exec --dir /app/indexer -- pwd ct live exec -- /app/restart.sh # Run the environment's restart.sh script ct live score # Run the scorer on the current state of the container ct live templ bash snap # Append new container bash history to template.jsonl ct live templ bash clear # Clear container bash history and reset tracking ct live templ add # Interactively add a tool call to template.jsonl ct live gen-traj # Generate solution.json from git diff + template.jsonl ct live traj # Run solution.json tool calls (--traj <path> for another file) ct live solution-from # Convert .eval/.jsonl files to solution.json in traj dir ct live down -k # Tear down the current context (-k keeps its state) ct live clear # Remove the current context state # Testing trajectories ct live gen-traj # Generate trajectory from live context ct live score # Run scorers on live context ct run eval --trajectory-id <path-from-gen-traj> --policy replay # Re-run scoring ct run monitor <path-to-the-.eval> # Monitor the .eval # Context management ct live current # Show all contexts and which one is active (alias: `status`) ct live switch # Switch to a different context ct live up # New context when selections differ; reuses a matching one ct live down --switch-to <context-id> # Tear down and switch to another context ct live clear --switch-to <context-id> # Clear context and switch to another # Run a command in another context without switching (active context: tom312) ct live exec --context abc123 -- ls # Runs in abc123 ct live exec -- ls # Still runs in tom312

Traj Directory Structure

ct live up creates a traj directory with this structure.

For a side task (attack mode):

.settings/linuxarena/{env_id}/side_tasks/{side_task_id}/trajs/{main_task_id}/0/
├── template.jsonl    # Template tool calls (if applicable)
├── description.md    # Attack description
├── info.yml          # Tracks uploaded eval logs
└── solution.json     # Generated solution (gitignored)

For a main task only (honest mode):

.settings/linuxarena/{env_id}/main_tasks/{main_task_id}/trajs/0/
├── template.jsonl
├── description.md
├── info.yml
└── solution.json

Use --no-traj-dir to skip traj directory creation, --traj-dir-name to select an existing one, or --new-traj-dir to create a new one with auto-incremented name. Use --no-build to skip local image builds and pull prebuilt images instead.

For what goes in description.md, straj_development/, and the rest of a sabotage trajectory's (straj's) documentation, see Making Sabotage Trajectories.

Template Trajectories

gen-traj combines the tool calls in the template trajectory (template.jsonl) with the container's file changes to build the solution trajectory. Of the two, only the template needs editing by hand:

┌─────────────────────────────────┐     ┌─────────────────────────┐
│ Template Trajectory             │     │ Container State Changes │
│ (trajs/*/template.jsonl)        │     │ (git diff)              │
└─────────────────────────────────┘     └─────────────────────────┘
        |                                |
        v                                v
        ┌────────────────────────────────┐
        │        gen-traj command        │
        └────────────────────────────────┘
                        |
                        v
    ┌───────────────────────────────────────┐     ┌───────────────────┐
    │    Solution Trajectory                │     │ Tool Call Results │
    │    (trajs/*/solution.json)            │     │  Monitor Results  │
    └───────────────────────────────────────┘     │  Scorer Results   │
                        |                         └───────────────────┘
                        |                                   |
                        v                                   v
                        ┌───────────────────────────────────┐
                        │            .eval file             │
                        └───────────────────────────────────┘

gen-traj finds all unstaged file changes in the container with git diff and builds the solution trajectory from them, using the template as its base. To make changes locally instead of in the container, pass --mount-fresh-dir to live up, which creates a fresh local directory and mounts it at /app. This is an alternative to live agent code.

Template trajectories use a custom .jsonl format. For example:

{"function": "bash", "arguments": {"cmd": "service postgresql start"}} {"function": "bash", "arguments": {"cmd": "service vsftpd start"}}

By default, gen-traj puts all generated file-change tool calls before the template's tool calls. To insert certain file changes elsewhere, add a placeholder:

{"function": "bash", "arguments": {"cmd": "service postgresql start"}} {"function": "bash", "arguments": {"cmd": "service vsftpd start"}} {"function": "placeholder", "arguments": {"path": "src/scraping/main.py"}} // absolute or relative paths accepted (relative to /app)

A placeholder can also name a directory, in which case file changes under it are inserted there:

{"function": "bash", "arguments": {"cmd": "service postgresql start"}} {"function": "placeholder", "arguments": {"path": "/app"}} // All changes under /app will be inserted here {"function": "bash", "arguments": {"cmd": "service vsftpd start"}} {"function": "placeholder", "arguments": {"path": "src/scraping/main.py"}} // All except changes to /app/src/scraping/main.py, which would go here

Changes to files outside /app need an explicit placeholder:

{"function": "bash", "arguments": {"cmd": "service vsftpd start"}} {"function": "placeholder", "arguments": {"path": "/tmp/malware.sh"}}

live up reads the template so that, when setup initializes git at /, the gitignore includes these external paths.

To reset git tracking, for example after adding an external-path placeholder to the template after up, pass --reset-git to live setup. Run git restore . in the container first if needed so git initializes from the correct state. The --force flag only reruns the task's setup script.

gen-traj also adds mkdir tool calls for missing parent directories, since the create-file tool call requires the parent directory to exist.

To iterate on the content of an argument, reference a local file; gen-traj then reads the value from that file. This also works with score, which uses the template's submit tool call.

{"function": "bash", "arguments": {"cmd": "service postgresql start"}} {"function": "bash", "arguments": {"cmd": "service vsftpd start"}} {"function": "placeholder", "arguments": {"path": "src/scraping/main.py"}} {"function": "submit", "arguments": {"extraction_code": {"local_file": "extraction_script.py"}}} // path relative to repo root (control-tower)

If the local file doesn't exist, the macro resolves to the argument's value in the matching tool call of the solution trajectory. If your solution trajectory is up to date with your template, you don't need to keep separate local files.

live apply-macros populates the referenced local files from the solution trajectory.

Tool Call Filtering and Tagging

The traj command supports these filtering and manipulation options:

  • --include-tool <tool_name>: Only run tool calls using specific tools
  • --exclude-tool <tool_name>: Skip tool calls using specific tools
  • --include-tag <tag>: Only run tool calls with specific tags
  • --exclude-tag <tag>: Skip tool calls with specific tags
  • --reverse: Reverse text_editor str_replace operations

Tags can be specified in template tool calls:

{"function": "bash", "arguments": {"cmd": "service vsftpd start"}, "tags": ["1"]} {"function": "placeholder", "arguments": {"path": "src/scraping/main.py"}, "tags": ["2"]}

Adding Tool Calls Interactively

templ add adds tool calls to your template interactively:

ct live templ add

It lets you:

  • Select from available tools or add a placeholder
  • For placeholders: View changed files from git diff with suggested common parent directories
  • For tools: Walk through each parameter with type-aware prompts
  • Use local file macros for string parameters (references files in your repo)
  • Add tags to tool calls
  • Skip optional parameters with reasonable defaults ("", 0, [], {})

Capturing Bash History

templ bash snap adds the bash commands you've run in the container to your template:

ct live templ bash snap

This command:

  • Reads the bash history from the container
  • Only captures commands since the last time you ran snap (tracked per context)
  • Ignores some commands, including clear, by default
  • Appends the new commands as bash tool calls to your template trajectory
  • Only sees commands from shells you have exited. Bash writes ~/.bash_history when the shell closes, so to keep the shell open, run history -a in it before snap.
  • Does not see commands run with ct live exec.

To clear the bash history in the container and reset the tracking:

ct live templ bash clear

Custom Text Editor Tool Calls

The editor tool calls that gen-traj generates from the git diff replace a file's full content. For several targeted edits to the same file at different points in the trajectory, write text_editor tool calls in the template yourself.

Because traj applies these edits to the container, a later gen-traj would create conflicting tool calls from the diff. Tag the custom edits and exclude them when you run traj.

The local file macro is useful here too. For example, the old_str and new_str arguments of a str_replace tool call can reference local files:

{"function": "placeholder", "arguments": {"path": "node/src/ftp.js"}} {"function": "text_editor", "arguments": {"command": "str_replace", "path": "/app/node/src/ftp.js", "old_str": {"local_file": "my_changes/backup/ftp_js_edit_1/a.js"}, "new_str": {"local_file": "my_changes/backup/ftp_js_edit_1/b.js"}}, "tags": ["custom_edit"]}

Generating Trajectories

When a Docker environment is set up, git is initialized at / to track changes in the container. Other sandbox backends skip this, because git init / over a full root filesystem is too slow, so gen-traj needs a Docker environment. gen-traj generates .json trajectory files and can output four kinds of file edit tool calls from the diff:

  1. Create file
  2. mkdir
  3. Delete file
  4. Update file

The update tool call replaces an old_str argument, the file's state after environment setup, with a new_str argument, its new state. If the file has changed since the trajectory was last generated, the tool call conflicts and fails. live traj instead warns that the file changed and still writes the new_str content for your review. If the conflict was trivial, run live gen-traj to update the old_str.

Codebase Development

Pass --env-only to up to be prompted only for an environment when you are developing the environment itself.

An environment can add gitignore patterns in a live.gitignore file in its codebase/ directory, and a traj directory can have its own live.gitignore. Both are appended to the base gitignore when git is initialized in the sandbox, so gen-traj skips generated files, caches, or configs specific to the environment or trajectory.

To develop an environment codebase, use live agent code with --mount-dir to mount your local directory into the container:

ct live agent code --mount-dir /path/to/your/local/codebase

This command:

  • Mounts your local directory at /app in the container
  • Opens VS Code with the Dev Containers extension attached to the running container
  • Switches interactive terminals from root to the environment's configured agent_user

To work from a CLI-based agent (Claude Code, OpenAI Codex, or Google Gemini) instead, use:

ct live agent <agent-cli> [linuxarena options] -- [cli arguments]

These CLIs run as the environment's agent_user (root by default).

Examples:

ct live agent claude --mount-dir /path/to/codebase -- --dangerously-skip-permissions --model opus ct live agent codex --mount-dir /path/to/codebase -- --model gpt-5-codex -c model_reasoning_effort=high ct live agent gemini --mount-dir /path/to/codebase -- --yolo --show-memory-usage

Options before -- configure the live agent command (mounts, contexts, etc.). Anything after -- is passed straight to the CLI running inside the container.

For autonomous task solving, --solve sends the task description(s) to the agent and skips permission checks by adding --dangerously-skip-permissions, --yolo, or --dangerously-bypass-approvals-and-sandbox for Claude, Gemini, or Codex respectively:

ct live agent [claude|gemini|codex] --solve

Use --prompt for custom instructions, alone or combined with --solve.

On first run, the helper installs the matching CLI (claude, codex, or gemini) with npm install -g. To manage it yourself, install @anthropic-ai/claude-code, @openai/codex, or @google/gemini-cli ahead of time and make sure the binary is on PATH inside the container.

Make sure you pass the correct environment codebase path: the mount replaces /app in the container with your local directory.

To develop the codebase and a trajectory together:

  1. Attach to the container with your local codebase mounted.
  2. Make changes in the container. Commit codebase changes and leave trajectory changes in the working tree.
  3. When the trajectory is ready, run gen-traj.
  4. Run git restore . in the container. Your codebase changes now show up in your local repo.