---
title: "Run Claude Desktop tasks on a Lens Agents sandbox — Lens Blog — Lens"
description: "Give everyone who uses Claude Desktop a sandbox for their tasks, with no open port, read-only web access, and a record of every request."
url: https://lenshq.io/blog/claude-desktop-on-lens-agents/
---

[← Blog](https://lenshq.io/blog/)

[Tutorials](https://lenshq.io/blog/category/tutorials/)

# Run Claude Desktop tasks on a Lens Agents sandbox

Jari Kolehmainen November 6, 2026 14 min read

**TL;DR**

- In the **Code** tab of Claude Desktop, anyone can give Claude a task that takes real work: research a market, clean up a spreadsheet, write a report, fix a bug. In a cloud session, the task continues when you close the laptop.
- By default, that work runs on Anthropic's machines. With [self-hosted environments](https://code.claude.com/docs/en/self-hosted-environments), it runs on a machine that your company operates. But that machine runs commands that a model writes, and the docs leave its hardening to you.
- A Lens Agents sandbox gives you that hardening in one spec file: no open port, rules for what the task can reach, and a record of every request. Share the sandboxes in a team, or give each person their own.

Claude Desktop is not only for developers. A researcher asks Claude to collect sources and compare them. A marketer asks for a competitor summary or a cleaned-up campaign report. In the **Code** tab, Claude does that work with a real computer: it runs commands, reads the web, and writes files.

Where that computer is matters. On your laptop, the task stops when you close it, and it can read everything that you can. In a cloud session, the task runs on Anthropic's machines, and your company has no say in what it can reach.

Self-hosted environments put that computer in your company. Claude Desktop stays the same for the user, and the work runs on a machine that your platform team operates. That machine then runs commands that a model writes. The Claude Code docs say what it needs: a dedicated host, no broad credentials, and default-deny egress. They also say that the product can't enforce the egress part for you.

A Lens Agents sandbox has all three from the start. This post makes one an environment that people can pick in Claude Desktop.

## How the pieces fit

A self-hosted environment has three parts:

- The **environment** is a name in your claude.ai organization. It shows in the environment selector of Claude Desktop, next to the environments that Anthropic hosts.
- The **runner** is a process on your machine. It is part of the Claude Code CLI, `claude`. It registers with the environment, waits for work, and starts one Claude Code process for each task.
- The **session** is one task. The user starts it and talks to it in Claude Desktop, as before.

![Claude Desktop sends a task to claude.ai. In the Lens Agents sandbox, the runner and one Claude Code process for each task send all traffic through the sandbox proxy, which connects only out: to Anthropic (api.anthropic.com) for registration, polling, the model and git, to the web for reading only, and to the internal services that you allow. The proxy adds the real environment key only to registration, and the token of an internal service only to that service.](https://lenshq.io/blog/claude-desktop-on-lens-agents/self-hosted-runner-flow.webp)

The runner only makes outbound HTTPS requests to `api.anthropic.com`. Anthropic never connects to it, so the sandbox needs no exposed port. The prompts and the model answers go through Anthropic. The commands, the downloads, and the files that the task makes stay in the sandbox.

The runner proves that it belongs to the environment with an environment key. The key never enters the sandbox: the sandbox holds a placeholder, and Lens Agents adds the real key at the boundary, only to the request that registers the runner. Each task then uses its own token from Anthropic.

Some tasks also work on a GitHub repository. For those, the runner asks Anthropic for git access to `github.com`, and Anthropic uses the GitHub connection of the person who started the task. The sandbox holds no GitHub credential.

## Shared or dedicated runners

An environment can have many runners, and any member of your claude.ai organization can send a task to any environment. You choose who the runners in it serve:

- **Shared runners** take the tasks of anyone in the organization. A runner serves one person at a time, but the next task can be from another person. A team shares a few sandboxes, and you add sandboxes when more tasks must run at the same time.
- **Dedicated runners** take the tasks of one person only. Each person gets their own sandbox, and all the files in it belong to that person.

Shared runners are the easier start. Use dedicated runners when tasks handle data that other people must not see. The Claude Code docs explain why: when a runner starts again on the same machine, the files of the last task can still be there.

The same spec does both. Don't mix the two in one environment, because a shared runner also takes the tasks of a person who has a dedicated runner. If you use both, create one environment for each.

## Who does what

Steps 1 to 3 are for the person who runs your Lens Agents platform. They do Steps 1 and 2 once, and Step 3 once for each sandbox. Step 4 is for the person who uses Claude Desktop, and it is the only step that they see.

The platform admin needs:

- A Claude **Team** or **Enterprise** plan. Self-hosted environments are a public beta. An Owner must turn on **Allow self-hosted environments** on the [Cloud environments](https://claude.ai/admin-settings/cloud-environments) admin page.
- A Lens Agents platform. If you don't have one yet, follow the [getting started guide](https://docs.lenshq.io/lens-agents/getting-started/).
- `nexusctl`, installed from your Lens Agents platform and logged in with `nexusctl auth login`.

The user needs Claude Desktop, signed in to the same claude.ai organization. For tasks on a GitHub repository, they also need a GitHub connection on their claude.ai account, and an Owner must enable the GitHub connector.

## Step 1: Create the environment

On the [Cloud environments](https://claude.ai/admin-settings/cloud-environments) page, under **Self-hosted environments**, select **New**. Give the environment a name that tells people what it is, such as `lens-agents-shared` or `lens-agents-dedicated`, and select **Create**. Then select **Copy environment key**.

The key is shown only once. Keep it in a password manager: you need it in Step 3 for each sandbox. It expires 365 days after you create it.

## Step 2: Define the sandbox

```
name: Claude Runner
image: ubuntu:24.04
command: sh /home/ubuntu/run-runner.sh
cpu: "4"
memory: 8Gi

user: ubuntu
workdir: /home/ubuntu

volumes:
  - mountPath: /home/ubuntu
    size: 30Gi

env:
  CLAUDE_CODE_VERSION: "2.1.311"
  CLAUDE_ACCOUNT: "${CLAUDE_ACCOUNT}"

files:
  - path: /home/ubuntu/run-runner.sh
    content: |
      delay=5
      while :; do
        rm -rf "$HOME/workspace"
        set -- --base-dir "$HOME/workspace" \
          --use-anthropic-git-proxy
        [ -z "$CLAUDE_ACCOUNT" ] || set -- "$@" --lock-to-account "$CLAUDE_ACCOUNT"

        started=$(date +%s)
        claude self-hosted-runner "$@"
        if [ $(( $(date +%s) - started )) -ge 60 ]; then delay=5
        else delay=$(( delay * 2 > 300 ? 300 : delay * 2 )); fi
        sleep "$delay"
      done

scripts:
  - when: pre-start
    description: install base packages
    user: root
    run: |
      apt-get update -qq
      DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
        curl git ca-certificates openssh-client jq python3 >/dev/null
  - when: pre-start
    description: give the home volume to the runner user
    user: root
    run: chown ubuntu:ubuntu /home/ubuntu
  - when: pre-start
    description: install Claude Code
    user: root
    run: |
      bin=/home/ubuntu/.local/share/claude/claude-$CLAUDE_CODE_VERSION
      if [ ! -x "$bin" ]; then
        mkdir -p "$(dirname "$bin")"
        curl -fsSL -o "$bin.tmp" \
          "https://downloads.claude.ai/claude-code-releases/$CLAUDE_CODE_VERSION/linux-x64/claude"
        chmod +x "$bin.tmp" && mv "$bin.tmp" "$bin"
        chown -R ubuntu:ubuntu /home/ubuntu/.local
      fi
      ln -sf "$bin" /usr/local/bin/claude

policy:
  name: Claude Runner
  networkDefaultVerdict: deny
  allowedDomains:
    - { pattern: api.anthropic.com, verdict: allow, transport: direct }
    - { pattern: "*.internal.example.com", verdict: deny, transport: direct }
    - { pattern: "10.0.0.0/8", verdict: deny, transport: direct }
    - { pattern: "172.16.0.0/12", verdict: deny, transport: direct }
    - { pattern: "192.168.0.0/16", verdict: deny, transport: direct }
    - pattern: "*"
      verdict: allow
      transport: direct
      description: Read-only internet
      rules:
        - { method: GET }
        - { method: HEAD }
  credentials:
    - { credentialName: environment-key, envVarKey: SELF_HOSTED_RUNNER_ENVIRONMENT_SECRET }

credentials:
  environment-key:
    value: ${ENVIRONMENT_KEY}
    injections:
      - domain: api.anthropic.com
        headerName: Authorization
        headerFormat: "Bearer {value}"
        rules:
          - { method: POST, path: /v1/code/runners/self-hosted/runners/register }
```

A few things to note:

- **The environment key is a credential.** `$SELF_HOSTED_RUNNER_ENVIRONMENT_SECRET` in the sandbox holds a placeholder, and the runner sends it as is. The injection rule adds the real key only to `POST /v1/code/runners/self-hosted/runners/register`. After that, the runner uses a token that the registration returns, and each task uses its own token, so the injection doesn't touch them.
- **The runner exits after each task.** That is by design: one runner serves one session. The start script starts it again. If the runner fails soon after it starts, the script waits longer each time, up to five minutes, as the Claude Code docs recommend.
- **`CLAUDE_ACCOUNT` selects the option.** Set it to an email address, and the script adds `--lock-to-account`, so the runner takes only the tasks of that person. Leave it empty, and the runner is shared.
- **Each task starts in an empty workspace.** Before the runner starts, the script deletes `~/workspace`, where the runner clones repositories and keeps the files of each task. Files that a task writes in other places, such as caches in the home directory, stay on the volume. With shared runners, the next person's task can read them, so use dedicated runners for sensitive data.
- **`--use-anthropic-git-proxy`** gives tasks git access to `github.com` through `api.anthropic.com`. It works only for repositories on `github.com`, and only for branch pushes. A push of a tag or a branch delete fails.
- **The task can read the web, but not write to it.** The policy denies by default. For the internet, only `GET` and `HEAD` pass, so Claude can read pages and download files, but it can't post a form or upload a file. The one exception is `api.anthropic.com`, which carries the model and the git pushes. If a task needs to write to a service, add that domain with the methods it needs.
- **Internal services are your choice.** The read-only rule for `*` also matches internal hosts and IP addresses that your Lens Agents platform can reach, and the first rule that matches wins. So the spec denies your internal domains and the private IPv4 ranges first. Replace `*.internal.example.com` with your internal domains. The pattern also covers `internal.example.com` itself. Add any other range that your network uses, such as `100.64.0.0/10`. The IP ranges stop requests to an address, but not to a name: a name outside your list that points to an internal address still passes. To let tasks use an internal service, such as a reporting API, add a rule for its domain above the deny rules. If the service needs a token, add it as a credential with an injection for that domain, as for the environment key. The task then uses the service, but never sees the token.
- **Claude Code is pinned.** `CLAUDE_CODE_VERSION` sets the version, and the binary is stored on the volume, so a restart doesn't download it again. The runner needs 2.1.224 or later, and the git proxy needs 2.1.267 or later. To upgrade, change the version and run `nexusctl sandbox update <sandbox> --project <org>/<project> -f sandbox.yaml`, with `ENVIRONMENT_KEY` and `CLAUDE_ACCOUNT` set as in Step 3.
- **The model comes from Anthropic.** The session uses a token that Anthropic issues for it, so it can't use Lens Agents managed inference. Your claude.ai plan pays for the model.

## Step 3: Create the sandbox

Read the environment key into your shell first:

```
read -rs ENVIRONMENT_KEY && export ENVIRONMENT_KEY
```

Paste the key at the prompt. `read -s` keeps it off the screen and out of your shell history. Use the key of the environment for this option.

For a shared runner, leave `CLAUDE_ACCOUNT` empty. Run the command again for each sandbox that you want in the team:

```
CLAUDE_ACCOUNT= nexusctl sandbox create --project <org>/<project> -f sandbox.yaml
```

For a dedicated runner, use the email address that the person uses for claude.ai:

```
CLAUDE_ACCOUNT=person@example.com nexusctl sandbox create --project <org>/<project> -f sandbox.yaml
```

`create` expands `${ENVIRONMENT_KEY}` and `${CLAUDE_ACCOUNT}` from your environment. The first start installs packages and downloads Claude Code, so it takes a minute or two. Then the runner registers:

```
nexusctl sandbox logs <sandbox> --project <org>/<project>
```

```
[self-hosted-runner] Registering as opted in to Anthropic-managed git (--use-anthropic-git-proxy)
[self-hosted-runner] Registered: runner_id=<runner-id>
```

On the Cloud environments page, the environment changes from **No runners deployed** to **Healthy**.

## Step 4: Start a task

This step is for the person who uses Claude Desktop.

Open the **Code** tab and start a new session. In the row under the prompt box, the first control is the environment selector. Select the environment there, such as `lens-agents-shared`. If the task works on a GitHub repository, select it too. Then ask Claude for what you need, for example:

```
Find the five most-cited papers on retrieval-augmented generation from the last two years. Summarize each one in three sentences, and write a comparison table to a Markdown file.
```

Claude works in the sandbox: it reads the web, runs commands, and writes files. You can close the laptop. The task continues in the sandbox, and you can follow it in the Claude mobile app or on [claude.ai/code](https://claude.ai/code).

When the task ends, the runner exits, the script starts a new one, and the environment is ready for the next task.

## What the sandbox adds

Claude Desktop gives people the interface. The sandbox decides what their tasks can do:

- **A computer off the laptop.** The task can't read the files, passwords, or cloud logins on the laptop. With dedicated runners, each person's tasks also run in their own sandbox.
- **No open port.** The runner only connects out. Nobody can reach it from the internet.
- **Read-only web, by default.** Only `api.anthropic.com` gets writes. Rules for each domain and each method, enforced outside the task. The Claude Code docs recommend default-deny egress for self-hosted runners, and they leave it to you. Here, it is part of the spec.
- **No secrets in the sandbox.** The environment key is added at the boundary, and Anthropic-managed git uses the GitHub connection of the person on claude.ai. A task can't read the key or send it anywhere. It can only use it to register a runner, as the runner does.
- **Audit.** Every request the sandbox proxy handles is recorded, with the policy decision. When someone asks what a task did on the network, you have the answer.

The workspace is deleted before each task, so ask Claude to put results that you want to keep where you can get them, such as a GitHub repository.

If you are a developer and you prefer to work in your editor, see [how to use VS Code and Claude Code on a Lens Agents sandbox](https://lenshq.io/blog/vscode-on-lens-agents/).

## Run your agents governed

Run Claude, Codex and other agents with identity, policy, and audit built in.

[Book a demo](https://lens-agents-contact.lenshq.io/)

## FAQ

**Do people need to know the command line or Git?**

No. They use Claude Desktop as before and select the environment. The platform admin does the setup.

**Does this work for normal chats and Cowork in Claude Desktop?**

No. Self-hosted environments are for the **Code** tab only. Normal chats and Cowork run as before.

**How do I add a person?**

With shared runners, you don't: anyone in the organization can use them. With dedicated runners, create a sandbox for the person from the same spec, with their email in `CLAUDE_ACCOUNT`, and give it the key of the dedicated environment.

**What happens if a person without a sandbox picks the dedicated environment?**

No runner is locked to them, so no runner takes their task. Create a sandbox for them first, or tell them to use the shared environment.

**How many tasks can run at the same time?**

One for each runner: the git proxy needs one task for each runner. With shared runners, add sandboxes to the team. With dedicated runners, create a second sandbox for the person.

**Does the sandbox need an exposed port?**

No. The runner only makes outbound HTTPS requests to `api.anthropic.com`. Anthropic never connects to the sandbox.

**How do I rotate the environment key?**

Create a new key on the environment's **Configuration** tab. For each sandbox of that environment, read the new key as in Step 3 and run `nexusctl sandbox update <sandbox> --project <org>/<project> -f sandbox.yaml`, with the same `CLAUDE_ACCOUNT` as before. The runner gets the new key the next time it registers, after the current task. When all sandboxes have the new key, revoke the old key.
