Skip to content
Draft, pre-release documentation

This tutorial connects Claude Code to a Chrome session in Kasm and has it complete a task on a real web page. Kasm Session Runtime runs inside the session as an MCP server. Claude Code does the reasoning and calls the runtime’s tools: it lists the page’s interactive elements, clicks one by the handle the listing gave it, and checks where it ended up.

The runtime needs no model for this, since Claude Code does the reasoning. The dom.* tools the task uses work without one; only the screen.ask, screen.ground and screen.judge tools need a vision endpoint configured in the session.

The run uses the 05-mcp-drive example in the SDK, and its INSTRUCTIONS.md, which is written for an agent to follow.

You need:

  • A Kasm Workspaces deployment you can reach.
  • A Kasm API key and secret that can set environment variables when it requests a session (request_kasm), plus the username of the user it acts as. The runtime switches on only when the session’s environment carries KASM_RUNTIME_SESSION_TOKEN, and Kasm drops that environment for a caller without an API key. The key also registers a port map on the image the first time, which needs the IMAGES_VIEW, IMAGES_MODIFY and IMAGES_MODIFY_RESOURCES permissions.
  • The username and password of a real Kasm user. Kasm’s reverse proxy lets a request through to the session’s MCP endpoint only with that user’s login cookie.
  • A kasm-nix Chrome image with the runtime built in. The example defaults to chrome:nix. The walkthrough was verified on the image published 2026-09-04.
  • Claude Code, uv and python3 on your machine.
  • Outbound internet access from the session: the task uses pcpartpicker.com.

In the SDK’s examples directory, copy settings.env.example to settings.env and fill in the Kasm block. Keep every line an export.

Terminal window
export KASM_URL=https://kasm.example.com
export KASM_API_KEY=...
export KASM_API_KEY_SECRET=...
export KASM_SESSION_USER=...
export KASM_USERNAME=...
export KASM_PASSWORD=...
export KASM_VERIFY_TLS=true

If you have only a username and password, the helper can mint a scoped API key from an account that holds global admin rights. Optional:

Terminal window
uv run lib/kasm_provision.py bootstrap-api-key

Leave the vision endpoint lines commented out. Without them, the screen.* tools answer with an error and the dom.* tools work as normal.

Terminal window
cd 05-mcp-drive
./session.sh up

The script:

  1. Registers the mcp port map (port 9434) on the image, once. Running it again changes nothing.
  2. Generates a random session token and requests a session with KASM_RUNTIME_SESSION_TOKEN set to it. The runtime in the image starts only when that variable is present.
  3. Opens the session’s viewer in your browser. Set NO_OPEN=1 to skip that on a machine with no browser; the link is printed either way.
  4. Prints the MCP connection details.

The output ends with a line you can paste:

Terminal window
claude mcp add --transport http kasm-session "https://<host>/desktop/<kasm_id>/mcp/mcp" \
--header "X-Kasm-Runtime-Session-Token: <token>" --header "Cookie: session_token=<...>; username=<...>"

Both headers are required. The token goes in X-Kasm-Runtime-Session-Token because Kasm’s proxy overwrites the Authorization header.

The script keeps the session id and token in .session in the same folder, so ./session.sh connect can print the details again and ./session.sh down can destroy the session from a fresh shell.

Run the claude mcp add line the script printed. Then start Claude Code in the same folder. If it needs to reconnect later, use /mcp inside Claude Code, or run ./session.sh connect for the details again.

Ask Claude Code to call dom.url with no arguments. On a fresh session it names Chrome’s new-tab page, chrome://new-tab-page/ or chrome://newtab/.

If it fails:

Error Meaning
401 The session token header is wrong
502 The runtime is not up yet (wait a few seconds), or the port map is missing

Point Claude Code at INSTRUCTIONS.md in the same folder, or give it the goal directly. The goal, from that file:

This page lists products in one category (e.g. CPUs). Call
dom.list_interactive to see the products, dismiss any cookie banner if
present, then click 'Add' on any one suitable product using
dom.click_described. Do not check whether other parts are already
selected -- that is not needed. Don't ask follow-up questions; use your best
judgement.

The page is https://pcpartpicker.com/products/cpu/. The session starts on a blank tab, so opening it with dom.navigate is part of the task and gets recorded like every other step.

The tools it will use:

Tool What it does
dom.navigate {url} Opens the page and returns once it has loaded
dom.list_interactive {filter?} Lists what is on the page as {handle, role, name, disabled}. On a busy page, pass a filter
dom.click_described {description} Clicks one element, by a handle from the listing it just got, or its exact name
dom.wait {predicate, timeout_s} Waits until a JavaScript predicate on the page is true
dom.url Returns where the tab is now
screen.judge {expectation} Asks the vision model whether something is visibly true. Needs a vision endpoint
screen.capture Takes a screenshot

A verified run took eight calls:

  1. dom.navigate to the category page.
  2. dom.list_interactive with filter “Allow”, then dom.click_described on the cookie button’s handle.
  3. dom.wait until the product list is back, because the consent click re-renders the page.
  4. dom.list_interactive with filter “Add”, then dom.click_described on one product’s Add button.
  5. dom.wait for /list/ in the URL, then dom.url, which reads https://pcpartpicker.com/list/.

That run ended with a screen.judge call. Without a vision endpoint that call returns an error, and the dom.url result is the confirmation.

Handles are five characters and come from the latest listing. If the agent guesses or reuses one, the runtime refuses the click with “not a handle on this page”.

The unfiltered listing on this page has more than 130 entries. A filter keeps the agent’s context small.

A click returns as soon as it is delivered, before the next page has loaded, so the sequence waits with dom.wait before reading dom.url.

Switch to the session’s viewer. The sidebar on the right lists every tool call Claude Code makes, with its result, as it happens. Anyone who opens the viewer later, while the session is still up, sees the run replayed.

If the sidebar says “Waiting for the runtime”, the runtime did not start: check that the API key is in settings.env, because without it the session token never reaches the session.

Do this whether the task succeeded or not:

Terminal window
./session.sh down
claude mcp remove kasm-session

A session left running holds a container and a seat. The MCP registration holds a Kasm login cookie, so remove it too.

  • The example does not collect a report file from the session. The sidebar is the only record.
  • It depends on a live public site. If pcpartpicker.com changes its layout, the goal may need rewording.
  • Any MCP client can connect to the same endpoint with the same two headers. session.sh up also prints a config block for another client alongside the Claude Code line.