Live Development

The recommended way to develop trajectories is through the ct live utility. It will spin up the defined Compose environment for your task, separately from Inspect, and allow you to iterate on your trajectory.

Run uv run ct live to see all available commands, and for each command pass --help to see its available arguments. You typically won't need to pass arguments to these commands, as they will prompt you for all necessary information. Session metadata is cached under .ct/sessions/ (one JSON file per session) with .ct/active pointing at the active one, so you won't need to unnecessarily re-enter information.

Disabling Interactive Prompts

The live utility supports interactive prompts (TUI) by default when running in a terminal (only when stdin.isatty() is true). To disable these prompts e.g. if you are using an AI agent in a TTY terminal, set the DISABLE_TUI environment variable:

# In .env file or environment DISABLE_TUI=1

When set, the live utility will skip all interactive prompts even if running in a TTY, and will either use provided arguments or fail with an error message indicating what arguments are needed.

MCP Server for AI Agents

The MCP server script exposes ct CLI tools as MCP tools, allowing AI agents like Claude Code to interact with Control Tower directly. This saves a lot of time since the agent doesn't need to figure out how the commands work by reading --help strings.

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

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

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

Commands marked with mcp_background_default = True in their Click definition will default to running in the background. Commands marked with mcp_unlisted = True are hidden from the MCP tool list.

Multi-Context Support

The live utility supports multiple concurrent contexts, allowing you to work on different environments, tasks, or trajectories simultaneously. Each context maintains its own Docker Compose project and state. You can:

  • Create multiple contexts: Run live up multiple times to create different contexts
  • Switch between contexts: Use live switch
  • View all contexts: Use live status to see all contexts and which one is active
  • Remove contexts: Use live down to pull down the compose project and remove the current context (use live clear if you only want to remove the context state)

Most commands support the --context <context-id> option to operate on a specific context instead of the active one; the active context is unchanged.

Quick Start from Eval Files or Traj Directories

You can quickly spin up a context from an existing .eval or .json file, or from a traj directory path using the up-from command:

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

This automatically extracts the environment, main task, and side task from the source, eliminating the need for manual selection. When using a traj directory path, it will also automatically select that traj directory.

Example workflow:

# Create and work with your first context uv run ct live up # Prompts for env, main task, side task; creates traj dir with template.jsonl, description.md, info.yml uv run ct live up-from path/to/file.eval # Auto-detect env, task, side task from .eval/.json file uv run ct live setup # Runs task's setup script and initializes container state for when `gen-traj` is run. Explicitly running setup with `live setup` is optional - live up will automatically run this (pass `--no-setup` to live up to disable this). This command is for debugging the setup script if there is one. # Methods for editing container files uv run ct live agent code # Attach to the agent container using dev containers. (Works in VS Code derived IDEs) uv run ct live agent claude # Launch Claude Code's CLI inside the container uv run ct live agent codex # Launch OpenAI Codex CLI inside the container uv run ct live agent gemini # Launch Google Gemini CLI inside the container uv run ct live shell # Pop a bash shell uv run ct live exec --dir /app/indexer -- pwd uv run ct live exec -- /app/restart.sh # Run the environment's restart.sh script uv run ct live score # Run the scorer on the current state of the container uv run ct live templ bash snap # Capture bash history from the container and append to template.jsonl (only new commands since last capture) uv run ct live templ bash clear # Clear container bash history and reset tracking uv run ct live templ add # Interactively add a tool call to template.jsonl uv run ct live gen-traj # Generate solution.json from git diff + template.jsonl uv run ct live traj # Run solution.json tool calls. Use --traj <path> for a different file. uv run ct live solution-from # Convert .eval/.jsonl files to solution.json in traj dir uv run ct live down -k # Tear down current context (pass `-k` to keep the context state) uv run ct live clear # Clear/remove the current context state # Testing trajectories uv run ct live gen-traj # Generate trajectory from live context uv run ct live score # Run scorers on live context uv run ct run rerun-traj --from-live # Re-run scoring on the generated trajectory uv run ct run monitor <path-from-gen-traj> # Monitor the trajectory # Context management uv run ct live current # Show all contexts and which one is active (alias: `status`) uv run ct live switch # Switch to a different context uv run ct live up # Create a new context if the current one is already running uv run ct live down --switch-to <context-id> # Tear down and switch to another context uv run ct live clear --switch-to <context-id> # Clear context and switch to another # Working with specific contexts # Run commands in different contexts without switching permanently: # Say you're in context `tom312` but want to run a command in `abc123`: uv run ct live exec --context abc123 -- ls # Run in abc123 without switching active context uv run ct live exec -- ls # Still runs in tom312 (active context unchanged)

Converting Trajectories

For single-trajectory conversion, use ct traj make.

# Convert eval log to one trajectory uv run ct traj make <eval_file> --sample-id <sample_id> --save-path ./data/traj.json # Convert JSONL/tool-call data to one trajectory uv run ct traj make <jsonl_file> --env <env> --save-path ./data/traj.json # Batch conversion (directory or single file) uv run ct runs make <eval_file_or_url> --save-dir ./trajectories/ # See all available options uv run ct traj make --help uv run ct runs make --help

Traj Directory Structure

When you run ct live up, a traj directory is automatically created with the following structure:

For side tasks (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 main tasks 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 detailed guidance on straj documentation structure (what goes in description.md, straj_development/, etc.), see Making Strajs.

Template Trajectories

Template trajectories (template.jsonl) define the tool calls that will be combined with container file changes to generate the solution trajectory. You should only need to manually edit the template as the solution is derived from the template and container state:

┌─────────────────────────────────┐     ┌─────────────────────────┐
│ 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             │
                        └───────────────────────────────────┘

So the gen-traj command will find all the unstaged file changes in the container via git diff, and build a solution trajectory from them using the template trajectory as a base/template. If you would rather make your changes locally (instead of directly in the container), you can pass --mount-fresh-dir to live up which will conveniently create a fresh local directory and mount to the live container at /app. This is a nice alternative to live agent code.

Template trajectories follow a custom .jsonl schema format. For example:

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

By default, when gen-traj is run, all of the automatically generated file change tool calls are put before the tool calls defined in the template. You can override this however by optionally specifying a placeholder for where you want certain file changes to be inserted in the solution traj:

{"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)

You can also specify directories so file changes under the specified directory will be inserted at the placeholder:

{"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

If you need to include changes to files outside of /app, you must specify them explicitly with a placeholder:

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

This is why the traj directory's template is read on live up. So the gitignore is properly configured to include external paths when git is initialized at / on setup.

If you ever need to reset the git tracking (e.g, if you add an external path placeholder to your template after running up), pass --reset-git to live setup. Just make sure you've run git restore . in the container if needed so git initializes at the correct state. Note that the --force flag only reruns the task's setup script.

When gen-traj is run, it automatically detects missing parent directories for files and adds the necessary mkdir tool calls (the create file tool call requires the parent dir to exist).

To help iterate on file content passed to certain arguments during development, you can pass a reference to a local file so when gen-traj is run, it reads from the referenced file. This also works with the score command, which will reference the submit tool call of the template.

{"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 will resolve to the value from the corresponding tool call in the solution trajectory. The system matches the tool call in your template with the same tool call in the solution trajectory and extracts the argument value from there. This means if your solution trajectory is up-to-date with your template, you don't need to maintain separate local files.

You can populate referenced local files using the content from the solution trajectory by calling live apply-macros.

Tool Call Filtering and Tagging

The traj command supports 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

The templ add command provides an interactive way to add tool calls to your template trajectory:

uv run ct live templ add

This command allows you to:

  • 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

The templ bash snap command provides an easy way to capture bash commands you've run in the container and add them to your template trajectory:

uv run 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

If you want to clear the bash history in the container and reset the tracking:

uv run ct live templ bash clear

Custom Text Editor Tool Calls

This gen-traj command which can create editor tool calls from a git diff is nice and convenient but what if you want to make multiple targeted changes to the same file across different points in your trajectory. Or if you would rather the str_replace call not replace the full file's content.

The issue is that traj applies these changes to the container, so when you later run gen-traj, it creates new tool calls from the git diff, potentially causing conflicts your custom edits. This is where tool call tagging comes in handy so you can run traj and exclude these custom edits.

The local file macro is also useful here. For example, you can have the old_str and new_str arguments for a str_replace tool call reference content from 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 an environment is set up, git is automatically initialized at the root (/) to track all relevant changes in the container for when gen-traj is run. gen-traj generates .json trajectory files and can output four kinds of file edit tool calls from this git diff:

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

The update file tool call works by "replacing" an old_str argument which reflects the original state of the file after environment setup with a new_str argument representing the new state. If the state of the underlying file in the environment has changed since the trajectory was last generated, there will be a conflict and the tool call will fail. However, when running live traj, you will simply get a warning that the state of the file has changed, but the file will still update with the new_str content for your review. If the conflict was trivial you can just run live gen-traj to update the old_str.

Codebase Development

You can also pass --env-only to up which will configure the context to only prompt for your environment if you are solely developing the environment.

Environments can define additional gitignore patterns by creating a live.gitignore file in their codebase/ directory. Traj directories can also define their own live.gitignore file. These files will be appended to the base gitignore configuration when initializing git in the sandbox. This is useful for ignoring environment-specific or trajectory-specific generated files, caches, or configs, so gen-traj doesn't register these changes and create file edit tool calls for them.

When developing environment codebases, we recommend using the live agent code command with the --mount-dir option to mount your local development directory into the container:

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

This command will:

  • Mount your local directory at /app in the container
  • Open VS Code with the Dev Containers extension attached to the running container
  • Automatically configure terminals to switch from root to the configured agent user

When you open an interactive terminal in VS Code, it will automatically switch to whatever agent_user is configured to be for the environment.

To work from a CLI-based editor (e.g. Claude Code, OpenAI Codex, or Google Gemini), use the pattern:

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

These CLI-based editors by contrast will run as the environment's agent_user (root by default).

Examples:

uv run ct live agent claude --mount-dir /path/to/codebase -- --dangerously-skip-permissions --model opus uv run ct live agent codex --mount-dir /path/to/codebase -- --model gpt-5-codex -c model_reasoning_effort=high uv run 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, use --solve to auto-prompt the agent with task descriptions and skip permission checks:

uv run ct live agent [claude|gemini|codex] --solve # Sends task description(s), adds --dangerously-skip-permissions / --yolo / --dangerously-bypass-approvals-and-sandbox

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

The helper will try to install the matching CLI (claude, codex, or gemini) with npm install -g the first time it runs. If you prefer to manage it yourself, install @anthropic-ai/claude-code, @openai/codex, or @google/gemini-cli ahead of time and ensure the binary is on PATH inside the container.

Make sure you specify the correct environment codebase path. The mount command will copy the files from you local machine to the container on mount.

You can also work on codebase and trajectory development in tandem. Example workflow:

  • Start by attaching to the container and mounting your local codebase
  • Then you can make changes as needed in the container
    • For codebase changes: Commit these changes
    • For trajectory changes: Keep these changes in the working tree
  • Once happy with the trajectory, run gen-traj
  • Then in the container run git restore .
  • You should see your codebase changes in your local repo!