Drive Contrast with your own agent

Hand the workspace to Claude Code, Codex, or anything that can run a shell command.

Contrast ships its own agents, and it will also step aside for yours. A drive session hands one external agent the project chat, the plan board, the worker squad, the web and iOS previews, and screenshots of the app actually running. Your agent calls the same tools Contrast’s own agents call, under the same authority, so what it gets is the real harness.

What you need

  • Contrast running on this machine. contrast ide and contrast drive reach a Contrast IDE tab over loopback, so drive mode needs a local Contrast rather than a hosted workspace.
  • A signed-in CLI. contrast login for a person, or CONTRAST_API_KEY for a headless agent. Set CONTRAST_ORIGIN when you want to name the Contrast you mean instead of relying on detection.
  • A project your account can open.
terminal
export CONTRAST_ORIGIN=https://contrast.localhost:3000
export CONTRAST_API_KEY=<token>
contrast whoami

Bind the checkout to a project

Two files, and they are not the same thing. contrast.json is the committed project pointer the whole team shares. .contrast/config.json is this clone’s own routing state and belongs in .gitignore.

terminal
contrast link --project <projectId> # writes contrast.json
contrast attach <projectId> --mode local # writes .contrast/config.json
contrast status

--mode local is what routes IDE and drive commands through the loopback daemon instead of the server-side attach tunnel.

Host a tab for the agent

Drive mode executes in a browser, because that browser is what holds the factory runtime claim that authorizes a project change. The claim is never copied into the CLI. So the first thing your agent does is give itself a tab:

terminal
contrast ide open --project <projectId>

It launches the tab, waits for it to register with the local daemon, and reports what it got:

opened a2 project=<projectId> user=<userId> url=<the project route>

The tab is an invisible Chrome that outlives the command, so there is no window in the way. That browser has its own profile and your normal session does not reach it, so the command signs the tab in as you, using the credential contrast login stored (or CONTRAST_API_KEY). With neither, it stops and tells you to run contrast login first. Expect the first open to take ten seconds or so, and pick your agent’s timeouts accordingly.

If you already have Contrast open and signed in on that project in your own browser on the same port, the same command connects to that tab instead of hosting a second one.

The tab is a resource your session owns. Close it when you are done, and see Troubleshooting if it does not go quietly.

Take the project

terminal
contrast drive start --as <name>
drive mode on as "<name>" — built-in Lux and Manager stood down; workers keep running

Lux stops answering the chat and the Manager stops planning, so your agent is the only thing making decisions. Everything else keeps going: workers, designers, pair review, and QA continue in the browser exactly as before. Messages from the person you are working for still land on the normal project thread, and your replies appear there as Contrast.

Starting is transactional, so a second session cannot also win. It is refused by name, with project is already driven by "<name>", and repeating start from the session that already holds the project just succeeds again.

Then look around:

terminal
contrast drive status # session, board, squad activity, recent chat
contrast drive tools # every tool name available
contrast drive tool create_file # one tool's description and JSON schema

contrast drive tool <name> is the fastest way out of a parameter mistake. It prints the real schema, which beats guessing at what --params wants.

Do the work

terminal
contrast drive call read_file --params '{"path":"package.json"}'
contrast drive call create_file --params '{"path":"app/pricing.tsx","content":"…"}'
contrast drive call app_screenshot \
--params '{"preview":"ios","surface":"agent"}' \
--image-out ./frame.webp
contrast drive reply "added the pricing screen, screenshot attached"
contrast drive reference ./frame.webp --message "this is what it looks like now"
contrast drive watch --timeout 300000

Four things worth knowing before your first call:

  • Every app tool names a surface. surface:"agent" is the hidden working preview your agent drives; surface:"visible" is the pane the person is looking at. They are different documents, so keep one value for a whole inspection flow rather than alternating.
  • --image-out writes exactly the bytes the tool returned, and app_screenshot returns WebP. Name the file .webp; a .png name gets WebP content and confuses whatever reads it next. The command tells you the type it wrote.
  • --agent <name-or-id> binds file and preview tools to that worker’s active branch, which is how you inspect or fix what one squad member is building instead of the main branch.
  • Reads never need a drive session, project changes always do. With nothing driving, contrast ide tool read_file runs fine and a tool that changes the project refuses by name:
error: create_file: this tool changes the project, and that needs the IDE's factory runtime claim. run `contrast drive` first (it stands the built-in Lux and Manager down), then run this again. read-only tools work without it.

Every contrast drive call wants a session of its own, reads included, since the session is the identity it runs as. Once you are driving, the two verbs do the same thing.

contrast drive watch blocks until the owner’s next message arrives, so an agent with nothing to do can park on it instead of polling.

Hand back

terminal
contrast drive stop
contrast ide close
drive mode off — built-in Lux and Manager may resume closed 1 hosted tab(s)

stop carries the session id your machine holds, so a stale process cannot release a newer driver’s claim. close shuts down only the hosted tabs this session started, matched by owner pid, and never touches a colleague’s tab or your own browser.

Troubleshooting

A write is refused right after the tab reloaded. A reload mints a new runtime claim while the old one is still held, and it clears itself within one 20-second lease:

error: drive: the Manager seat is still held by this project's previous IDE runtime; it is released within 20s, then this command works. retry.

Retry. It heals on its own, and no drive stop or fresh tab is needed.

Every verb suddenly fails with a Zero initialization error. After a long session with a lot of hot reloading, the tab’s data binding can come loose and reads start failing along with writes. The fix is contrast ide reload host, which reloads the browser page itself. contrast ide reload is the wrong tool here, because it rebuilds the app preview through the same binding that is broken.

No tab is connected. Nothing is hosting one, or it exited. Every drive verb refuses with the exact contrast ide open command for your project and port, so you can run it straight back.

Two tabs are connected. The CLI never guesses which one you meant. Pass --project <projectId>.

Your Contrast is not on port 3000. Pass --http-port <n> to every contrast ide and contrast drive command; they default to 3000 and do not read the port from the attach config.

contrast ide close says no hosted tab belongs to this session. Each host logs to /tmp/contrast-ide-tab-hosts/, named for the port it serves. That log names the host pid and which arm ended it.

Next

  • Agents, how the built-in factory frames and lands a goal, and what your agent is standing in for.
  • The Factory space, the board and squad your drive session is writing to.
  • GitHub sync, if you want the work to land as pull requests.

Ready?

Create a web, iOS, and Android app in minutes with agents working alongside you.