Development Setup
Use main for development only. The main branch is the active integration branch and is not considered stable; released versions are published through PyPI and GitHub release tags after checks pass.
Clone the repository and set up the development environment:
git clone git@github.com:linuxarena/control-tower.git cd control-tower make init
make init runs uv sync --dev --all-extras, creates .env from .env.example, activates the repo git hooks, and offers to install shell tab completions and an alias ct='uv run ct'.
Edit .env to add at least one LLM API key, such as ANTHROPIC_API_KEY or OPENAI_API_KEY.
Generate the model rate table so eval costs are priced rather than unknown:
uv run python scripts/generate_model_rates.py
The file lands at ~/.cache/control-tower/model_rates.json. Override the path with CONTROL_TOWER_MODEL_RATES in .env if needed.
Re-running the script merges into that file rather than replacing it: published rates are refreshed, new models are added, and a model the source has stopped publishing keeps the rate it last had (the script reports how many). To price a model the source does not carry, add the row by hand and mark it "manual": true — the script leaves those rows alone. It also refuses to write a table the reader could not load back, so a typo in a hand-added row is reported rather than taking the whole table down.
{ "model": "our-internal-model", "manual": true, "pricing": {"input_per_million": 3.0, "output_per_million": 15.0, "cached_input_per_million": 0.3} }
Pulling Settings
Settings are directories under .settings/ that group one or more environments—realistic software systems that agents are evaluated in (e.g., a Flask API, a Clojure job scheduler, a C++ network monitor). The .settings/ directory will be empty after a fresh clone. Official environments are hosted as separate repos under the linuxarena GitHub org, but you can also create your own. To pull official ones:
uv run ct settings pull # pull public envs from all tracked settings uv run ct settings pull linuxarena # pull public LinuxArena environments uv run ct settings pull linuxarena-private # public + private LinuxArena environments uv run ct settings pull linuxarena --env web_scraping
Check installed environments and their status:
uv run ct settings status
ct settings pull clones public environment repos anonymously with git clone. For private repos (e.g. the linuxarena-private set) it falls back to gh repo clone, so the GitHub CLI (gh) must be installed and authenticated only when you need those.
Tracked settings are defined in src/control_tower/environments/settings.yml. You can create custom sets in .settings.local.yml at the repo root (see src/control_tower/environments/settings.local.yml.example).
Local changes are always protected. ct settings pull never touches an environment whose working tree has uncommitted changes or untracked files — commit, stash, or ct settings remove it to resume updates.
Machine-level store. ct settings pull --global pulls into ~/.cache/control-tower/settings (honoring XDG_CACHE_HOME) instead of this checkout's .settings/. A checkout with no .settings/ of its own resolves environments from that store, so new checkouts and git worktrees — of Control Tower or of any consumer repo — need no per-checkout pull while the store is current. A local .settings/ always wins over the store, and CONTROL_TOWER_SETTINGS_DIR overrides both. Every ct settings command follows the same resolution, so in a checkout without .settings/ a plain ct settings pull updates the store too; create an empty .settings/ first to give a checkout its own copies.
Pinning. ct settings pull <target>@<ref> pulls at that tag/branch/SHA and records the pin per-machine — every later pull keeps the env there until you release it:
uv run ct settings pull linuxarena@v2.0.1 # pin a whole setting uv run ct settings pull linuxarena/tiktok@abc1234 # pin a single env uv run ct settings pull linuxarena@default # release, back to declared refs
Team-wide pins are ref: edits in settings.yml, reviewed in a PR like any other change. (Pinning code does not pin the prebuilt Docker image — set ENV_IMAGE_TAG_<ENV> in .env for that.)
Local environments (no GitHub repo needed): ct settings pull ./my_env symlinks the directory into .settings/ and records the link, so it survives future pulls. The source directory is never modified. ct settings remove my_env unlinks it.
Per-machine pins and links live in .settings.local.yml (pins: / links:), written by the CLI and hand-editable.
Local Services
A single Docker Compose file spins up support services such as MongoDB, the web viewer, and the inspect logs viewer:
docker compose up -d --build
See Web Viewer, Inspect Logs Viewer, and Docent for service ports, credentials, and environment variables.
Shell Tab Completion
make init offers to install tab completion. To run the completion installer on its own:
uv run python scripts/setup_completions.py
This will install a marked block in ~/.zshrc and/or your bash rc file that will load completions when a new shell is opened.
To uninstall, run:
uv run python scripts/setup_completions.py --uninstall
Agent Skills
Domain skills (running-evals, monitoring, making-*, safety-curves, trajectories, control-concepts, validate-straj) live in .agents/skills/ and symlinked in .claude/skills/.
The dev-workflow skills (pr, babysit, ship, deliver) ship as the control-tower-dev plugin from the control-tower-plugins marketplace — this repo's committed .claude/settings.json installs it when you trust the repo and auto-updates it at startup. Other plugins are opt-in: /plugin install control-research@control-tower-plugins. For anything beyond that (developing, publishing, or adding plugins), see that repo's README.
Codex discovers .agents/skills/ natively and consumes the same marketplace, once per machine: codex plugin marketplace add linuxarena/control-tower-plugins, then codex plugin add control-tower-dev@control-tower-plugins. Codex does not auto-update marketplace snapshots — refresh with codex plugin marketplace upgrade control-tower-plugins.
Editing control-tower from a downstream project
To iterate on control-tower while working in a downstream repo (basharena, research, etc.), from inside the downstream project root:
uv add --editable /path/to/control-tower
To switch back when done:
uv add 'control-tower @ git+https://github.com/linuxarena/control-tower.git@main'
Git Hooks
This repository has pre-commit and pre-push git hooks, activated by make init. To activate them on their own:
git config core.hooksPath .githooks
Note: The git hooks require bash 4.0 or later.
You may also add custom pre-commit and pre-push hooks by setting environment variables:
PRE_COMMIT_CUSTOM="/path/to/script.sh"PRE_PUSH_CUSTOM="/path/to/script.sh"
Pycache
Compiled python files in __pycache__ folders can interfere with git operations. To avoid this problem, store pycache files in an external folder by setting this environment variable:
export PYTHONPYCACHEPREFIX=/tmp/pycache
If you have any lingering __pycache__ folders in this directory, clean them up with
find . | grep -E "(/__pycache__$|\.pyc$|\.pyo$)" | xargs rm -rf
You can also use make clean to do the same thing.
Ruff import sorting vs CI
Stale __pycache__ folders can also cause ruff import sorting to disagree with CI. When a package is removed or renamed, its __pycache__ folder can linger as a phantom directory under src/. Ruff auto-detects anything under src/ as a first-party package, so these phantom directories cause ruff --fix to sort imports differently from CI (which has a clean clone). If ruff locally disagrees with CI on import sorting, this is likely the cause.
To fix, run make clean (documented above) and also clear the ruff cache with rm -rf .ruff_cache.
Note: make clean removes __pycache__ folders and .pyc/.pyo files, but it does not remove empty directories left behind after deletion. If you still see ruff disagreements after cleaning, check for and manually delete any empty directories under src/.