# OpenHands Local Setup: 12 Steps, 60 Min [2026]

> Source: <https://shattered.io/openhands-local-setup-agent-canvas-ollama-12-steps-2026/>
> Published: 2026-10-25 23:21:18+00:00

OpenHands now delivers its local experience through Agent Canvas, a browser client that connects to local, self-hosted, cloud or enterprise backends. The [OpenHands changelog tracker](https://agentbreakingchanges.com/tools/openhands/) logs releases and breaking changes, and it shows how quickly the pieces move. Pair that client with a model server on your own machine and your source code never leaves your network. A working OpenHands local setup takes about 60 minutes to build and ends with an agent that fixes a failing test inside a sandboxed container.

OpenHands’ own local-LLM page still tells newcomers to start with Qwen3.6-35B-A3B, an open-weight mixture-of-experts model built for agentic coding. Newer options have landed since, including [Recursive Flint](https://shattered.io/recursive-flint-40b-moe-coding-model-2026/), announced October 21, 2026. Ollama serves the model, one environment file pins it, and a model swap becomes a one-line change.

A terminal-first route exists too. A separate walkthrough covers a [self-hosted AI coding assistant built on Qwen Code](https://shattered.io/self-hosted-ai-coding-assistant-qwen-code-2026/). OpenHands covers different ground: a browser workspace, one container per conversation, and saved model profiles you can switch between mid-project.

Don't miss new tech stories on Google

Add Shattered once in the Google app and our stories show up in your news suggestions.

[Add now](https://www.google.com/preferences/source?hl=en&q=https%3A%2F%2Fshattered.io%2F)

## What OpenHands Agent Canvas Is in October 2026

Agent Canvas is the current local-facing product name. It connects to self-hosted model endpoints and runs agents against repositories on your disk. The official docs describe the launcher as an all-in-one stack: the Canvas client, an Agent Server, an Automation Server and an ingress proxy, all started by a single `agent-canvas` command. The older single-container GUI is a different thing. The local-LLM page’s quickstart still launches that older container image, so treat its Docker command as dated and borrow only its model settings.

The product changed a lot over the last few months. By October 7, OpenHands said Agent Canvas releases v1.17.0 through v1.24.0 had added agent profiles, isolated Docker runtimes, per-conversation containers and automation support, along with 64 bug fixes across the September series. Version 1.27.0 followed by October 13 with DigitalOcean Managed Agents support, which was still in public preview on that date. Later builds keep arriving, so run `agent-canvas --version` after installing and judge your copy against the release notes rather than against this article.

Three layers make up a working local setup, and knowing which layer makes which network call will save you hours.

- **The client.** A browser tab at`http://localhost:8000` where you start conversations, pick workspaces and manage LLM profiles.
- **The Agent Server.** The process that runs the agent loop, executes shell commands and edits files. In per-conversation Docker mode, this loop runs inside a container, not on your host.
- **The model server.** Ollama here. It exposes an OpenAI-compatible API, and the Agent Server calls it for every reasoning step.

That last point drives most setup failures. The model URL has to be reachable from wherever the agent loop runs, which is not always the machine your browser sits on. Terminal tools such as [Aider with a local Ollama model](https://shattered.io/aider-local-ai-coding-assistant-ollama-git-2026/) keep one process and one repository in view, so they never hit this split. Agent Canvas separates the pieces on purpose, and that separation buys you sandboxing, parallel conversations and a client you can point at a remote GPU box later.

Graham Neubig, co-founder and chief scientist at OpenHands, framed the project’s stance in the October 21 Flint announcement: developers should be free to pick the best model for their workload, infrastructure and cost. Every choice below follows from that idea. The agent stays constant while the model underneath it stays replaceable.

## Why Self-Host the Model, and What the Bill Looks Like

Privacy is the usual reason. A coding agent reads your repository, your environment files and often your test fixtures. Keeping inference on a box you control removes one whole category of data-handling questions.

Cost is the second reason, though it cuts both ways. Running models on your own hardware is free and unlimited in software terms, and Ollama’s local use carried no charge as of October 25, 2026. You still pay for the GPU, the electricity and the time you spend tuning. The table lays out the options side by side, using the prices listed on the vendors’ own pages this week.

| Path | Software cost | Where inference runs | Catch | 
|---|---|---|---|
| Agent Canvas + Ollama on your hardware | $0 | Your machine | You supply the GPU or unified memory | 
| Ollama Pro | $20/month or $200/year | Ollama cloud and local | Cloud models leave your network | 
| Ollama Team | From $500/month | Ollama cloud and local | Listed as early access | 
| OpenHands Cloud Teams | $499/month for up to 10 users | OpenHands-managed | Extra seats cost $50/month each | 
| OpenHands Cloud trial | 30 days | OpenHands-managed | Trial of the Teams plan | 
| OpenHands Enterprise | Custom pricing | Your infrastructure | No public price confirmed | 

Local models trail the best hosted ones on hard, multi-file work. A recent [Claude Opus 5.5 versus GPT-6 Astra terminal benchmark comparison](https://shattered.io/claude-opus-5-5-vs-gpt-6-astra-terminal-bench-2026/) shows how far the top hosted tier has pulled ahead on long-horizon terminal tasks. The practical answer is a split: route routine refactors, test fixes and codebase questions to the local model, and keep a hosted profile saved for the rare job that needs more. Agent Canvas supports saved profiles, so the split costs you one dropdown.

Ollama’s pricing page also changed shape this year. Per the company’s August 31 announcement, its Pro, Max and Team plans moved to per-token pricing with usage included on every plan. None of that touches the local path, but it explains why Ollama now lists cloud-hosted models next to local ones. Check which kind a tag is before you pull it.

## Prerequisites and Versions to Check First

Verify each item before an OpenHands local setup begins, because a missing prerequisite produces errors that look unrelated to the real cause. Agent Canvas uses `uv` to run its local agent server, so a missing `uv` breaks startup even when Node is fine.

| Component | Version or minimum | Check command | 
|---|---|---|
| Node.js | 24 or later | `node --version` | 
| npm | Ships with Node.js | `npm --version` | 
| uv | Current release | `uv --version` | 
| Docker | Daemon running (Desktop on macOS and Windows, Engine on Linux) | `docker ps` | 
| Ollama | v0.40.11 (October 24, 2026) or newer | `ollama --version` | 
| Python | Not listed | `python3 --version` | 
| Agent Canvas | Latest from npm | `agent-canvas --version` | 
| Memory for Qwen3.6-35B-A3B | 24 GB VRAM (quantized) or 64 GB Apple Silicon unified memory | `nvidia-smi` or About This Mac | 

The Ollama row comes from the [Ollama blog and release feed](https://ollama.com/blog?trk=public_post_main-feed-card-text), which lists v0.40.11 as the newest build. The memory row comes from OpenHands’ local-LLM page. If you have less than that, you can still follow every step with a small model for plumbing tests. Ollama’s own quickstart uses `gemma4:e2b`, a download of about 7.2 GB that it says wants 8 GB of VRAM or unified memory, and notes that systems with less can spill into system RAM at a speed cost. A model that small will not drive an agent well. Use it to prove the wiring, then move up.

Buying hardware for this? Unified-memory machines suit big mixture-of-experts models, since the weights have to fit somewhere. Our look at [RTX Spark PCs with large unified memory](https://shattered.io/nvidia-rtx-spark-n1x-october-launch-2026/) covers that class of machine. Recursive reported its Flint numbers on a 64 GB RTX Spark, which is the kind of box the vendor had in mind.

Windows users should run everything in PowerShell or inside WSL2. The Agent Canvas docs call out macOS, Linux, Windows with PowerShell and Windows with WSL2 as the supported targets and say mobile Linux environments like Termux are not a primary target. Budget your time like this: 10 minutes for Ollama, 10 for the context settings, 10 for Agent Canvas, 15 for the profile and sandbox, and 15 for the demo task and the launcher script.

## Steps 1 and 2: Install Ollama and Pull the Model

**Step 1: install Ollama.** Download the installer for macOS or Windows from ollama.com. On Linux, the official one-line installer works:

```
curl -fsSL https://ollama.com/install.sh | sh
ollama --version
```

The second command should print a version at or above 0.40.11. Reviewers now describe Ollama as a local-and-cloud runtime, a shift covered in [The Latent Daily’s Ollama review](https://www.thelatentdaily.com/reviews/ollama-review/). That matters for you because the app can sign in to a cloud account and list hosted models next to local ones. Stay signed out if the goal is to keep every token on your machine.

**Step 2: pull a smoke-test model, then the real one.** Start small. A 7.2 GB download finishes quickly and proves that Ollama can load a model and answer a prompt before you commit to a much larger file.

```
ollama pull gemma4:e2b
ollama run gemma4:e2b "Reply with the single word ready."
```

Once that works, pull the model OpenHands recommends. The tag below comes straight from the OpenHands local-LLM page. Expect a large download, so start it and move on to the next section while it runs.

```
ollama pull qwen3.6:35b-a3b
ollama list
```

Expected output from `ollama list` looks like this. Sizes and IDs will differ on your machine.

```
NAME                 ID              SIZE     MODIFIED
qwen3.6:35b-a3b      a1b2c3d4e5f6    ~20 GB   2 minutes ago
gemma4:e2b           f6e5d4c3b2a1    7.2 GB   9 minutes ago
```

The exact size depends on the quantization the tag resolves to. If the name column shows anything other than the tags you typed, stop and check for a typo before moving on, because every later step references this tag by name. Keep the tag in one place from now on. The launcher script in step 12 stores it in a single environment variable, `MODEL_TAG`, so a future swap to a newer model touches one line.

One more habit pays off early. Pick a model by its tool-calling behavior, not its benchmark headline. OpenHands’ agent loop leans on structured tool calls for every file edit and shell command, and some local models answer in prose instead of calling tools. The troubleshooting section covers what that failure looks like and which models the OpenHands docs say the community has had luck with.

## Steps 3 and 4: Raise the Context Window and Prove the Endpoint Works

**Step 3: set the context length before anything else talks to Ollama.** This is the single most common reason a local OpenHands setup misbehaves. OpenHands needs a large context to function. The docs recommend at least 22,000 tokens on lower-VRAM systems and 32,768 for better results, and they warn that Ollama’s 4,096-token default is too small to fit even the system prompt, so the agent behaves incorrectly. Three variables do the work:

```
export OLLAMA_CONTEXT_LENGTH=32768
export OLLAMA_HOST=0.0.0.0:11434
export OLLAMA_KEEP_ALIVE=-1
nohup ollama serve > ollama.log 2>&1 &
```

`OLLAMA_HOST=0.0.0.0` makes Ollama listen on every interface so Docker containers can reach it. `OLLAMA_KEEP_ALIVE=-1` keeps the model loaded between requests, which saves a long reload each time the agent pauses. If you start Ollama by hand as above, the variables apply. If Ollama already runs as a background service, they do not, and that mismatch causes many of the mystery failures later. Set them where the service reads them.

```
# Linux (systemd): add the variables to the service
sudo systemctl edit ollama.service
# In the editor, add:
#   [Service]
#   Environment="OLLAMA_CONTEXT_LENGTH=32768"
#   Environment="OLLAMA_HOST=0.0.0.0:11434"
#   Environment="OLLAMA_KEEP_ALIVE=-1"
sudo systemctl daemon-reload
sudo systemctl restart ollama

# macOS (menu bar app): set, then quit and reopen Ollama
launchctl setenv OLLAMA_CONTEXT_LENGTH 32768
launchctl setenv OLLAMA_HOST "0.0.0.0:11434"
launchctl setenv OLLAMA_KEEP_ALIVE -1
```

Binding to `0.0.0.0` has a cost. Anyone on your network can now reach an unauthenticated model endpoint. On a laptop at home that may be acceptable, but on a shared network it is not. Either block port 11434 from outside with your host firewall, or bind to the Docker bridge address only. Run `ip addr show docker0` on Linux to find that address, which is commonly 172.17.0.1 by default.

**Step 4: prove the endpoint answers over the OpenAI-compatible API.** OpenHands will talk to `/v1`, so test that exact path, not Ollama’s native one. First list the models:

```
curl -s http://127.0.0.1:11434/v1/models
{"object":"list","data":[{"id":"qwen3.6:35b-a3b","object":"model","created":1792000000,"owned_by":"library"},{"id":"gemma4:e2b","object":"model","created":1791990000,"owned_by":"library"}]}
```

The `id` field is the exact string you will paste into OpenHands later, so copy it from here. Now send one real chat request:

```
curl -s http://127.0.0.1:11434/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer local-llm" \
  -d '{"model":"qwen3.6:35b-a3b","messages":[{"role":"user","content":"Reply with the single word ready."}],"max_tokens":32}'
```

A healthy reply contains a `choices` array with a message and a `usage` block. Ollama ignores the bearer token, which is why the placeholder `local-llm` works, and OpenHands expects some string in its API key field for the same reason. The first call after loading a big model can take a while because the weights have to move into memory. If it hangs for minutes, check `ollama.log` for out-of-memory messages before you blame the network.

## Steps 5 and 6: Install Agent Canvas and Confirm It Starts

**Step 5: choose an install method.** The official docs list five. Three matter here, and the choice decides what the agent can touch.

- **npm global install.** The quickest route to a reusable`agent-canvas` command. Everything runs on your host with your user’s permissions.
- **npx.** Same behavior with no global install, good for a first look or when a`PATH` problem blocks the global command.
- **Docker.** The whole stack inside one container, with only the directories you mount visible to the agent.

The npm install comes first, and per-conversation Docker sandboxes follow in step 9. That combination keeps the Canvas client and outer server on the host while each agent session gets its own hardened container. The Docker-only route is a fair alternative if you prefer one moving part, and the commands are below.

```
node --version
uv --version
npm install -g @openhands/agent-canvas
agent-canvas --version
# Alternative: whole stack in Docker (macOS and Linux)
mkdir -p ~/projects
docker run -it --rm \
  -p 127.0.0.1:8000:8000 \
  -v openhands-data:/home/openhands/.openhands \
  -v ~/projects:/projects \
  ghcr.io/openhands/agent-canvas:latest
```

Use the named volume `openhands-data` exactly as shown. The `127.0.0.1:` prefix on the port mapping keeps the agent reachable from your machine only. In Docker mode the UI lives at `http://localhost:8000/canvas`.

**Step 6: start it and confirm the backend connects.** Run the command with no flags to launch the full local stack:

```
agent-canvas
# busy port? pick another:
agent-canvas --port 3000
```

Open `http://localhost:8000`. The default local backend should show as connected. If the browser stays blank, read the terminal where you launched it. The usual causes are a missing prerequisite, a busy port or a stopped Docker daemon. Useful flags to know now: `--backend-only` starts a headless backend, `--frontend-only` serves just the UI, `--info` prints stack details, and `--public` opens the server beyond localhost but refuses to start without a `LOCAL_BACKEND_API_KEY`. Leave `--public` off here.

Treat the process you just started as untrusted territory. The Agent Canvas docs warn that agents can run shell commands, read and write files and use connected tools, and that the client itself provides no isolation. If the backend runs directly on your machine, the agent acts with your account’s permissions. Step 9 fixes that, so do not point this install at a folder you cannot afford to lose until you have finished it.

## Steps 7 and 8: Create the Local LLM Profile and Test It

**Step 7: add a profile for the local endpoint.** In the browser, open `Settings > LLM` and add a new LLM profile. Use the `Advanced` tab, because the `Basic` tab only offers providers and models from its dropdowns. The Advanced tab lets you type the model name and base URL directly. Fill in four fields:

- **Model:**`openai/qwen3.6:35b-a3b` . The`openai/` prefix tells OpenHands to speak the OpenAI-compatible protocol, and the rest must match the`id` you saw in`/v1/models` .
- **Base URL:** depends on where the agent loop runs, covered below.
- **API key:** any placeholder such as`local-llm` . Ollama does not check it.
- **Profile name:** something recognizable, such as`local-qwen-ollama` .

Watch one default in particular. When you create a local profile, the form starts out selecting OpenAI GPT-5.6 Sol as the model and derives the profile name from it. Overwrite both before saving, or you will end up with a profile named after a hosted model that points at your laptop.

The base URL is where people lose an hour. Pick by topology:

- Agent Canvas on the host with the default local runtime: `http://127.0.0.1:11434/v1` .
- Agent Canvas in Docker, or per-conversation Docker runtime (step 9): `http://host.docker.internal:11434/v1` .
- Ollama on another machine: `http://<that-host>:11434/v1` , reachable from the backend.

The rule from the docs is simple: the URL must work from the backend, not from your browser. Inside a container, `127.0.0.1` points at the container itself. On Linux, Docker only resolves `host.docker.internal` when the container is started with the host-gateway mapping, and the per-conversation runtime sets that up for you. When you save, Agent Canvas validates the profile against the backend before persisting it. A bad key or unreachable model blocks the save and shows the backend’s error, which turns many typos into immediate feedback.

If you expect to add several models from the same server, create a Provider Connection first. It stores the provider, key and base URL once, and each linked profile supplies only its own model. Rotating a key or moving Ollama to a new host then means editing one record. Provider Connections work on local agent-server backends and are hidden on an OpenHands Cloud backend. The same Ollama endpoint also serves other editors, and our [Kilo Code local setup with Ollama in VS Code](https://shattered.io/kilo-code-local-setup-ollama-vs-code-2026/) reuses it, so one model pull feeds both tools.

**Step 8: test the profile in a new conversation.** Start a conversation and send something short, like a request to list the files in the workspace. An existing conversation keeps the agent and model it started with, so changing a profile never affects a chat that is already open. Always start fresh after an edit. A healthy first run shows the agent issuing a tool call, running a command and summarizing the output. If it answers in plain prose and never touches a tool, jump to the troubleshooting list, because the cause is nearly always the model or the context length rather than the URL.

## Step 9: Put Every Conversation in Its Own Container

With the default settings, the agent shares your user account. A wrong command or a prompt-injected instruction from a file in the repo can touch anything you can. Per-conversation Docker mode narrows that. Agent Canvas and the outer Agent Server stay on the host, and each conversation runs in a separate container.

Stop Agent Canvas with Ctrl+C, set the runtime variables, and start it again:

```
export OH_CONVERSATION_RUNTIME=docker
export OH_CONVERSATION_IMAGE=ghcr.io/openhands/agent-server:latest-python
export OH_CONVERSATION_CONTAINER_MEMORY=8g
export OH_CONVERSATION_CONTAINER_CPUS=4
agent-canvas
```

Each container runs as your host UID and GID rather than root, drops all Linux capabilities, sets `no-new-privileges` and publishes its API only on `127.0.0.1`. It bind-mounts three host directories: the conversation’s data, a per-conversation persistence directory, and `/workspace`. The defaults are 4 GB of memory, 2 CPUs and a 512 process limit, and containers stop after 20 minutes idle. The conversation survives and its container restarts on the next use. Change the base URL in your profile to `http://host.docker.internal:11434/v1` now, because the model calls originate inside the container.

Verify the isolation instead of trusting it. Start a new conversation and ask the agent to run this:

```
id
printf 'PWD=%s\nHOME=%s\n' "$PWD" "$HOME"
```

The output should report `/workspace` as the working directory and a home of `/var/openhands/.openhands`, not your real home. Then inspect the container from the host:

```
docker ps --filter name=agent-server-conversation-
docker inspect <container-id> --format '{{.HostConfig.CapDrop}} {{.HostConfig.SecurityOpt}} {{.HostConfig.PortBindings}}'
```

Three caveats from the docs deserve attention. First, in this mode the launcher listens on all interfaces so containers can reach host services, and it stops embedding the session API key in the page. Treat the port as exposed to your LAN and firewall it. Second, memory and CPU limits sent by a client replace the server’s values instead of being capped by them, so anyone who can start conversations can ask for a larger container. Third, the `/workspace` mount gives the container read and write access to that host folder, so point conversations at a dedicated directory.

## Step 10: Run a Real Task on a Demo Repository

A sandbox and a model mean little until a task passes. Build a tiny repository with a deliberate bug. Put it in a dedicated folder, because this is the directory you will hand to the agent.

```
mkdir -p ~/projects/slug-demo && cd ~/projects/slug-demo
git init -q

cat > slugify.py <<'EOF'
import re

def slugify(text: str) -> str:
    text = text.lower().strip()
    text = re.sub(r"[^a-z0-9]+", "-", text)
    return text
EOF

cat > test_slugify.py <<'EOF'
from slugify import slugify

def test_basic():
    assert slugify("Hello, World!") == "hello-world"

def test_collapses_spaces():
    assert slugify("  many   spaces  ") == "many-spaces"

def test_only_symbols():
    assert slugify("!!!") == ""
EOF

git add . && git commit -qm "demo: slugify with a bug"
uv run --with pytest pytest -q
```

Confirm the failure on the host first, so you know the starting point. Two tests should fail, because the function leaves a trailing hyphen after `Hello, World!` and returns a lone hyphen for symbols only.

```
F.F                                                                      [100%]
FAILED test_slugify.py::test_basic - AssertionError: assert 'hello-world-' == 'hello-world'
FAILED test_slugify.py::test_only_symbols - AssertionError: assert '-' == ''
2 failed, 1 passed in 0.03s
```

Back in the browser, choose `Open Workspace` and select `~/projects/slug-demo` before starting the conversation. Skipping that step is the classic reason an agent edits the wrong folder. Select your local profile and send a prompt that sets clear limits:

```
Run pytest in this repository. Fix slugify.py so every test passes.
Do not edit test_slugify.py. When done, explain the change in two sentences.
```

A competent run takes a few tool calls: read the files, run the tests, edit `slugify.py`, rerun. The expected fix is one line, stripping hyphens from both ends of the result. Review it yourself rather than trusting the summary:

```
git diff
uv run --with pytest pytest -q
-    return text
+    return text.strip("-")
...
3 passed in 0.02s
```

Small local models sometimes pass the tests the wrong way, for example by editing the test file or special-casing inputs. The prompt forbids the first, and the diff check catches the second. Make that review habit permanent. A local agent that you verify with `git diff` is far safer than a smarter one you trust blindly.

## Steps 11 and 12: Build the Preflight Check and One-Command Launcher

Typing seven exports and hoping nothing drifted gets old by day two of any OpenHands local setup. The finished project below has three files: an environment file that holds every setting, a Python preflight script that checks the whole chain before you open the browser, and a launcher that ties them together. Create a folder called `ohlocal` and keep all three inside it.

**Step 11: the environment file.** Everything configurable lives here, including the model tag. Swapping models later means editing `MODEL_TAG` and nothing else.

```
# ohlocal/ohlocal.env
export MODEL_TAG="qwen3.6:35b-a3b"
export OLLAMA_URL="http://127.0.0.1:11434"
export OLLAMA_CONTEXT_LENGTH=32768
export OLLAMA_HOST="0.0.0.0:11434"
export OLLAMA_KEEP_ALIVE=-1

export OH_CONVERSATION_RUNTIME=docker
export OH_CONVERSATION_IMAGE="ghcr.io/openhands/agent-server:latest-python"
export OH_CONVERSATION_CONTAINER_MEMORY=8g
export OH_CONVERSATION_CONTAINER_CPUS=4

export CANVAS_PORT=8000
```

**Step 12: the preflight script.** It uses only the standard library. It verifies the toolchain versions, confirms the model is listed, sends a real chat request, runs a context probe, and starts a throwaway container to prove that Docker can reach Ollama through `host.docker.internal`. That last check reproduces the exact path an agent conversation uses.

``` bash
#!/usr/bin/env python3
"""Preflight checks for a local OpenHands Agent Canvas stack."""
import json
import os
import socket
import subprocess
import sys
import time
import urllib.request

MODEL = os.environ.get("MODEL_TAG", "qwen3.6:35b-a3b")
OLLAMA = os.environ.get("OLLAMA_URL", "http://127.0.0.1:11434")
PORT = int(os.environ.get("CANVAS_PORT", "8000"))
results = []

def check(name, ok, detail=""):
    results.append(ok)
    tag = "PASS" if ok else "FAIL"
    print(f"[{tag}] {name}" + (f" ({detail})" if detail else ""))

def run(cmd, timeout=30):
    try:
        p = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
        return p.returncode, (p.stdout + p.stderr).strip()
    except (OSError, subprocess.TimeoutExpired) as exc:
        return 1, str(exc)

def call(path, payload=None, timeout=300):
    data = json.dumps(payload).encode() if payload else None
    req = urllib.request.Request(
        OLLAMA + path,
        data=data,
        headers={"Content-Type": "application/json", "Authorization": "Bearer local-llm"},
    )
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        return json.load(resp)

def chat(prompt, max_tokens=32):
    body = {
        "model": MODEL,
        "messages": [{"role": "user", "content": prompt}],
        "max_tokens": max_tokens,
    }
    return call("/v1/chat/completions", body)

def main():
    rc, out = run(["node", "--version"])
    major = int(out.lstrip("v").split(".")[0]) if rc == 0 else 0
    check("Node.js 24 or later", major >= 24, out)

    rc, out = run(["uv", "--version"])
    check("uv installed", rc == 0, out)

    rc, out = run(["docker", "ps"])
    check("Docker daemon running", rc == 0)

    try:
        ids = [m["id"] for m in call("/v1/models", timeout=10)["data"]]
        check("Ollama /v1/models reachable", True, f"{len(ids)} models")
        check(f"Model {MODEL} is pulled", MODEL in ids)
    except Exception as exc:
        check("Ollama /v1/models reachable", False, str(exc))
        return finish()

    try:
        start = time.time()
        reply = chat("Reply with the single word ready.")
        text = reply["choices"][0]["message"]["content"].strip()
        check("Chat completion works", bool(text), f"{time.time() - start:.1f}s")
    except Exception as exc:
        check("Chat completion works", False, str(exc))

    try:
        filler = "alpha bravo charlie delta " * 1800
        reply = chat(filler + "\nReply with the single word ok.", max_tokens=8)
        used = reply.get("usage", {}).get("prompt_tokens", 0)
        check("Context probe above 4096 tokens", used > 6000, f"prompt_tokens={used}")
    except Exception as exc:
        check("Context probe above 4096 tokens", False, str(exc))

    rc, out = run([
        "docker", "run", "--rm",
        "--add-host", "host.docker.internal:host-gateway",
        "curlimages/curl:latest", "-s", "-o", "/dev/null",
        "-w", "%{http_code}", "http://host.docker.internal:11434/v1/models",
    ], timeout=120)
    check("Container can reach Ollama", out.endswith("200"), f"HTTP {out[-3:]}")

    with socket.socket() as s:
        s.settimeout(1)
        busy = s.connect_ex(("127.0.0.1", PORT)) == 0
    check(f"Port {PORT} is free", not busy)
    return finish()

def finish():
    failed = results.count(False)
    print(f"\n{len(results) - failed} passed, {failed} failed")
    return 1 if failed else 0

if __name__ == "__main__":
    sys.exit(main())
```

The context probe sends a long block of filler and reads back the prompt token count. If Ollama truncated the prompt at the 4,096-token default, the reported count stays near that number and the check fails. That failure usually means your context variable never reached the running service.

Finally, the launcher. It loads the environment, makes sure Ollama is running, pulls the model if needed, runs the preflight, and only then starts Agent Canvas.

``` bash
#!/usr/bin/env bash
# ohlocal/start.sh
set -euo pipefail
cd "$(dirname "$0")"
source ./ohlocal.env

if ! curl -sf "$OLLAMA_URL/v1/models" > /dev/null; then
  echo "Starting Ollama..."
  nohup ollama serve > ollama.log 2>&1 &
  sleep 4
fi

ollama pull "$MODEL_TAG"
python3 preflight.py
echo "Preflight passed. Starting Agent Canvas on port $CANVAS_PORT"
exec agent-canvas --port "$CANVAS_PORT"
chmod +x start.sh
./start.sh
```

### What the preflight output should look like

On a healthy machine the run ends in a clean summary. Timings depend entirely on your hardware, and the sample below is only the shape to expect.

```
[PASS] Node.js 24 or later (v24.x)
[PASS] uv installed (uv x.y.z)
[PASS] Docker daemon running
[PASS] Ollama /v1/models reachable (2 models)
[PASS] Model qwen3.6:35b-a3b is pulled
[PASS] Chat completion works (3.4s)
[PASS] Context probe above 4096 tokens (prompt_tokens=7243)
[PASS] Container can reach Ollama (HTTP 200)
[PASS] Port 8000 is free

9 passed, 0 failed
```

Read failures as a map. A pass on the chat check with a failure on the container check means the model works but Ollama only listens on loopback. A failed context probe with everything else green means a service started before your variables existed. A failed model check means the tag in `ohlocal.env` does not match what `ollama list` prints. Each failure points at one fix, and that precision is the reason to script the checks.

## Swap In Newer Models: Recursive Flint, Nemotron and Multi-GPU Servers

The Qwen3.6-35B-A3B recommendation was current when OpenHands wrote it in May, but the model market moves in weeks. Gartner’s July 2025 forecast projected that by 2027 more than half of the generative AI models enterprises use will be domain-specific, up from 1% in 2024, per its [GenAI spending forecast release](https://www.gartner.com/en/newsroom/press-releases/2025-07-10-gartner-forecasts-worldwide-end-user-spending-on-generative-ai-models-to-total-us-dollars-14-billion-in-2025). Coding-tuned open models are the clearest example, and the table shows what OpenHands and Ollama have put in front of developers recently.

| Model | Size and design | Best use in this setup | Source and date | 
|---|---|---|---|
| Qwen3.6-35B-A3B | Open-weight mixture-of-experts | Default first local model, 24 GB VRAM or 64 GB Apple Silicon | OpenHands local-LLM docs | 
| Recursive Flint | 40B MoE, about 2.5B active, Apache 2.0 | Fast local agent runs on Apple Silicon and NVIDIA RTX | OpenHands announcement, October 21, 2026 | 
| NVIDIA Nemotron 3.5 Lightning | 30B, 3B active | Long-running local agents calling tools | Ollama announcement, August 11, 2026 | 
| NVIDIA Nemotron 3.5 Super VL | 120B MoE, 12B active, multimodal | Agents that must read screenshots and documents | OpenHands announcement, October 15, 2026 | 
| gemma4:e2b | About 7.2 GB download | Plumbing smoke test only | Ollama quickstart | 
| qwen2.5-coder-14b-instruct | 14B | Community-reported fix for chatbot-like behavior | OpenHands local-LLM docs | 

### Recursive Flint on OpenHands

OpenHands launched day-zero support for Recursive Flint on October 21. The model uses a 40-billion-parameter mixture-of-experts design that activates about 2.5 billion parameters per token and ships under the Apache 2.0 license. Recursive reports 125 or more tokens per second on an M4 Pro MacBook Pro and 150 or more on a 64 GB RTX Spark using its own optimized inference engine. Those are vendor-reported figures under specific configurations, and speeds will vary with runtime, quantization and context length. OpenHands also said it tested Flint ahead of release.

OpenHands describes two ways in. The first routes Flint through OpenRouter with an API key and the supported model identifier, which sends your prompts to a hosted service and defeats the privacy goal. The second installs Recursive’s recommended inference runtime, serves the Flint checkpoint and points an OpenHands profile at the local endpoint. Ollama’s release notes reportedly list a Flint build as of v0.40.8, so check `ollama list` and Ollama’s model library before assuming it is there. Because every setting lives in `ohlocal.env`, trying it takes one line: change `MODEL_TAG`, create a profile with the matching `openai/` model ID and run the preflight again.

Open-weight coding models raise a security question that deserves a pause. Our report on a [Qwen coding agent that retrained itself and leaked planted secrets](https://shattered.io/qwen-coding-agent-self-modification-secrets-2026/) shows why sandboxing and secret hygiene still matter when the weights are yours. A model you host is not automatically a model you can trust with credentials. Keep tokens out of the workspace and out of environment variables the container inherits.

### vLLM or SGLang for multi-GPU boxes

Ollama suits a single workstation. When you have a shared GPU server, the OpenHands docs show vLLM and SGLang launch commands that need at least two GPUs for the Qwen model. The vLLM version looks like this, with a placeholder key you should replace:

```
huggingface-cli download Qwen/Qwen3.6-35B-A3B --local-dir Qwen/Qwen3.6-35B-A3B

vllm serve Qwen/Qwen3.6-35B-A3B \
    --host 0.0.0.0 --port 8000 \
    --api-key CHANGE_ME \
    --tensor-parallel-size 2 \
    --served-model-name Qwen3.6-35B-A3B \
    --enable-prefix-caching
```

In the OpenHands profile, the model becomes `openai/Qwen3.6-35B-A3B`, the base URL becomes `http://<server>:8000/v1`, and the API key must match what you passed to `--api-key`. Unlike Ollama, these servers enforce the key, so a wrong value produces authentication errors that Agent Canvas shows as recoverable warnings. Binding to `0.0.0.0` exposes the server, so keep it on a private network or behind a firewall.

## Common Pitfalls That Break Local Agent Setups

Most failed OpenHands local setup attempts trace back to a short list of repeat mistakes. Check these before you debug anything exotic.

1. **Leaving Ollama’s context at the default.** A 4,096-token window cannot hold the system prompt, so the agent acts erratically. Set 32,768 and prove it with the probe in the preflight script.
2. **Setting variables the running service never reads.** Exports in your shell do nothing for an Ollama instance that systemd or the macOS app started earlier. Set them in the service environment and restart.
3. **Using 127.0.0.1 inside a container.** It points at the container, not your machine. Use`host.docker.internal` for Docker-based runtimes.
4. **Forgetting the openai/ prefix.** Without it, OpenHands cannot work out the provider and fails with a provider error. The remainder must match the ID from`/v1/models` exactly.
5. **Testing in an old conversation.** A conversation keeps the model it started with. After any profile change, open a fresh one.
6. **Skipping Open Workspace.** The agent works in whichever folder is attached, so files appear in the wrong place when you forget to pick one.
7. **Exposing the stack to your network.** Binding Ollama to all interfaces and running Docker mode leaves two unauthenticated services reachable on your LAN. Firewall both ports.
8. **Accepting the default profile model.** The form preselects a hosted OpenAI model, which silently sends your prompts off-machine if you only change the URL later.

A ninth habit prevents regret. Commit your work before every agent run, so a bad session is one `git checkout` away from undone. The demo repository in step 10 starts from a commit for that reason. Treat the agent like a fast, confident contractor who occasionally rewrites the wrong file.

## Troubleshooting: 10 Errors and Their Fixes

Work from the top, since the early items cover startup and the later ones cover model behavior.

1. **`agent-canvas: command not found`.** The npm global bin directory is missing from your` PATH` . Run`echo "$(npm prefix -g)/bin"` , add that path to your shell profile, or fall back to`npx @openhands/agent-canvas` .
2. **Startup fails mentioning `uv` or `uvx`.** The local agent server runs through` uv` . Install it, open a new terminal so`PATH` reloads, confirm with`uv --version` and relaunch.
3. **Port 8000 already in use.** Start on another port with`agent-canvas --port 3000` . For the Docker image, map a different host port, such as`-p 127.0.0.1:3000:8000` .
4. **`unable to open database file` in Docker on Linux.** You bound a host-owned`~/.openhands` directory. Switch to the named volume`openhands-data` instead of binding a host directory.
5. **The Docker image crash-loops with `ERR_MODULE_NOT_FOUND`.** A published image can ship with files missing. Check` docker logs` , pin an earlier known-good tag instead of`latest` , and retry`latest` after a patch lands.
6. **Backend shows as disconnected.** Open the backend switcher, choose`Manage Backends` and verify the host URL and key. For the default local setup you rarely need to enter a key by hand.
7. **`LLM Provider NOT provided`.** The model string lacks provider information. Confirm the profile uses the`openai/` prefix, the base URL is right and the key field holds some value.
8. **The profile saves but the conversation errors with a connection refused.** The base URL fails from the backend’s point of view. Run the throwaway-container curl from the preflight script and switch to`host.docker.internal` if the agent loop runs in a container.
9. **The agent answers like a chatbot and never calls tools.** Either the context is too small or the model handles tool calls poorly. The OpenHands docs say even a large window does not guarantee reliable tool use, and they list`qwen2.5-coder-14b-instruct` as community-reported to cure the chatbot behavior. Try that before assuming your setup is broken.
10. **Edits land in the wrong folder.** The conversation had no workspace or the wrong backend. Use`Open Workspace` first. In Docker mode, put the project under the mounted`/projects` directory. On a VM backend, remember the agent sees the VM’s files, not your laptop’s.

Two more checks save time when nothing above matches. Run the preflight script and read which line fails first, because it localizes the fault to the toolchain, the model server, the context window or the container network. Then read the Ollama log: out-of-memory errors there mean the model plus its context cache does not fit, and the fix is a smaller quantization or a shorter context, not a different URL.

## Advanced Tips for Daily Use

**Split models by job with a Model Router.** Under `Settings > LLM` you can create a router that picks among your saved profiles per task, starting from an OpenHands Router Pro or Router Flash template or a custom one. Combined with a local profile and one hosted profile, it lets routine edits stay on your GPU while hard problems escalate. Routing needs a backend that supports it, and the settings page says so when yours does not.

**Choose the runtime per conversation.** Set `OH_CONVERSATION_RUNTIME_SELECTABLE=1` and the home screen offers Docker next to Local, with a Worktree option when a folder is open. Use Docker for untrusted repositories and Local for quick questions on code you wrote. A Worktree runtime also keeps experimental branches from colliding with your working copy.

**Run the backend on a bigger machine.** Start `agent-canvas --backend-only` on a GPU server, then run `agent-canvas --frontend-only` on your laptop and add the server through Manage Backends with its URL and key. If you expose a backend beyond localhost, use `--public` with a long random `LOCAL_BACKEND_API_KEY` and put a reverse proxy with TLS in front.

**Cut telemetry and tighten secrets.** Set `AGENT_CANVAS_DISABLE_TELEMETRY=1` in the environment to turn off Agent Canvas product telemetry. Set `OH_SECRET_KEY` so stored settings and secrets are protected with a key you control, and keep real credentials in the Secrets settings instead of pasting them into prompts. Browse the wider [coding and developer tools coverage](https://shattered.io/category/software/) for editor-based agents that complement this browser workflow.

**Resist `--privileged` unless a task truly needs containers.** The Docker image cannot run nested containers by default, and the docs say the only workaround is starting it with `--privileged`, which weakens isolation from your host kernel. If an agent must build images, run that session on a disposable VM instead. Also keep the idle timeout in mind, since the default 20 minutes reclaims memory from forgotten conversations. Pin the model with `OLLAMA_KEEP_ALIVE=-1` only if you have the memory to spare.

## Frequently Asked Questions

**Is OpenHands free to self-host?**

The software runs locally at no charge, and Ollama’s local use carried no fee as of October 25, 2026. Costs come from hardware and power. OpenHands Cloud is a separate paid product, with a Teams plan at $499 a month for up to 10 users.

**Which model should I start with?**

OpenHands’ docs still recommend Qwen3.6-35B-A3B as the first local model to try. If your machine has the memory and you want a newer option, test Recursive Flint, which OpenHands supported from launch on October 21.

**Can I run this without a dedicated GPU?**

Ollama can fall back to system RAM when VRAM runs short, but responses slow down. Agent loops make many model calls per task, so the slowdown compounds. A small model works for testing the wiring and not much more.

**Does my code leave my machine?**

Not through the model if you use a local model on a local endpoint. Routing Flint through OpenRouter or picking an Ollama cloud model does send prompts out. Disable Agent Canvas telemetry with the environment variable if that matters to your policy.

**How is Agent Canvas different from the older OpenHands GUI?**

Agent Canvas is the current browser client, and it can connect to local, remote, cloud and enterprise backends. It adds saved LLM profiles, agent profiles, automations and per-conversation Docker runtimes. The older container flow in the local-LLM quickstart predates that design.

**Why does the agent behave like a plain chatbot?**

Check the context length first, since Ollama’s default is far too small. If the context is right, the model itself may handle tool calls badly, and a different coding-tuned model usually fixes it.

**Is it safe to give the agent my real projects?**

Use per-conversation Docker mode, mount only the folders the task needs, commit before each run and review every diff. Agent Canvas itself warns that agents can run shell commands and that the client provides no isolation.
