* Removed network access docs * Simplified docs for sandboxes and inference * Fixed build * docs(inference): clarify local inference routing * docs(inference): update provider and model examples * docs: add Docker prerequisite warning for connection-refused error Made-with: Cursor * Minor edits to quickstart safety and privacy * Doc restructured * removed tutorials * Added tutorial * Inference section reformatting * Updated CLI ref * removed troubleshooting * Updated inference * Updated pip install command for bug bash * Updated arch diagram * Updated tutorial * Updated install commands * Updated getting started * Updated brev link --------- Co-authored-by: Piotr Mlocek <pmlocek@nvidia.com>
11 KiB
Set Up a Sandbox with GitHub Repo Access
Agents often need to work across multiple repositories with different levels of trust. An agent might need full read-write access to a feature repo where it commits code, but only read access to a shared library repo that it references without modifying. OpenShell policies let you express this distinction — the agent can clone and push to one repo while treating another as a read-only dependency.
This tutorial sets up exactly that scenario:
- alpha-repo — the agent's working repo. Full read-write access: clone, push, create PRs and issues.
- bravo-repo — a reference repo. Read-only access: clone and browse, but push and write API calls are denied.
Access is locked to these two repos. The agent cannot clone, fetch, or call the API for any other repository on GitHub.
By the end you will have:
- A GitHub credential provider injecting your token into the sandbox
- A policy that extends the default with scoped GitHub access to exactly two repos
- A running sandbox where Claude Code, OpenCode, and the
ghCLI can all interact with GitHub
Prerequisites
Before you begin, make sure you have:
- Completed the {doc}
Quickstart </about/get-started>(CLI installed, Docker running) - A GitHub personal access token (PAT) with
reposcope, exported asGITHUB_TOKEN - Your agent's API key set (e.g.,
ANTHROPIC_API_KEYfor Claude Code)
Step 1: Create a GitHub Provider
:::{admonition} Already have a sandbox running? :class: tip
If you followed the Quickstart and already have a sandbox without a GitHub provider, you have two options:
- Add a provider to a new sandbox — delete the existing sandbox, create the provider below, and recreate the sandbox with
--provider my-githubin Step 3. - Set the token inside the sandbox — connect with
nemoclaw sandbox connect <name>and runexport GITHUB_TOKEN=<your-token>. This skips the provider workflow but the token is not persisted across sandbox recreations. :::
Create a provider that reads your GitHub token from the environment:
$ nemoclaw provider create --name my-github --type github --from-existing
This reads GITHUB_TOKEN (and GH_TOKEN if set) from your shell and stores them in the provider. The sandbox receives these as environment variables at runtime.
For more on provider types, see {doc}/sandboxes/providers.
Step 2: Write the Policy
Create a file called github-policy.yaml. This policy starts from the {doc}default policy </reference/default-policy> and replaces the GitHub blocks with scoped rules that grant read-write access to alpha-repo and read-only access to bravo-repo.
Replace <org> throughout with your GitHub organization or username.
version: 1
# ── Static (locked at sandbox creation) ──────────────────────────
filesystem_policy:
include_workdir: true
read_only:
- /usr
- /lib
- /proc
- /dev/urandom
- /app
- /etc
- /var/log
read_write:
- /sandbox
- /tmp
- /dev/null
landlock:
compatibility: best_effort
process:
run_as_user: sandbox
run_as_group: sandbox
# ── Dynamic (hot-reloadable) ─────────────────────────────────────
network_policies:
# Claude Code ↔ Anthropic API
claude_code:
name: claude-code
endpoints:
- { host: api.anthropic.com, port: 443, protocol: rest, enforcement: enforce, access: full, tls: terminate }
- { host: statsig.anthropic.com, port: 443 }
- { host: sentry.io, port: 443 }
- { host: raw.githubusercontent.com, port: 443 }
- { host: platform.claude.com, port: 443 }
binaries:
- { path: /usr/local/bin/claude }
- { path: /usr/bin/node }
# NVIDIA inference endpoint
nvidia_inference:
name: nvidia-inference
endpoints:
- { host: integrate.api.nvidia.com, port: 443 }
binaries:
- { path: /usr/bin/curl }
- { path: /bin/bash }
- { path: /usr/local/bin/opencode }
# ── GitHub: git operations (clone, fetch, push) ──────────────
github_git:
name: github-git
endpoints:
- host: github.com
port: 443
protocol: rest
tls: terminate
enforcement: enforce
rules:
# alpha-repo: clone, fetch, and push
- allow:
method: GET
path: "/<org>/alpha-repo.git/info/refs*"
- allow:
method: POST
path: "/<org>/alpha-repo.git/git-upload-pack"
- allow:
method: POST
path: "/<org>/alpha-repo.git/git-receive-pack"
# bravo-repo: clone and fetch only (no push)
- allow:
method: GET
path: "/<org>/bravo-repo.git/info/refs*"
- allow:
method: POST
path: "/<org>/bravo-repo.git/git-upload-pack"
binaries:
- { path: /usr/bin/git }
# ── GitHub: REST API ─────────────────────────────────────────
github_api:
name: github-api
endpoints:
- host: api.github.com
port: 443
protocol: rest
tls: terminate
enforcement: enforce
rules:
# GraphQL API (used by gh CLI)
- allow:
method: POST
path: "/graphql"
# alpha-repo: full read-write (PRs, issues, comments, etc.)
- allow:
method: "*"
path: "/repos/<org>/alpha-repo/**"
# bravo-repo: read-only
- allow:
method: GET
path: "/repos/<org>/bravo-repo/**"
- allow:
method: HEAD
path: "/repos/<org>/bravo-repo/**"
- allow:
method: OPTIONS
path: "/repos/<org>/bravo-repo/**"
binaries:
- { path: /usr/local/bin/claude }
- { path: /usr/local/bin/opencode }
- { path: /usr/bin/gh }
- { path: /usr/bin/curl }
# ── Package managers ─────────────────────────────────────────
pypi:
name: pypi
endpoints:
- { host: pypi.org, port: 443 }
- { host: files.pythonhosted.org, port: 443 }
- { host: github.com, port: 443 }
- { host: objects.githubusercontent.com, port: 443 }
- { host: api.github.com, port: 443 }
- { host: downloads.python.org, port: 443 }
binaries:
- { path: /sandbox/.venv/bin/python }
- { path: /sandbox/.venv/bin/python3 }
- { path: /sandbox/.venv/bin/pip }
- { path: /app/.venv/bin/python }
- { path: /app/.venv/bin/python3 }
- { path: /app/.venv/bin/pip }
- { path: /usr/local/bin/uv }
- { path: "/sandbox/.uv/python/**" }
# ── VS Code Remote ──────────────────────────────────────────
vscode:
name: vscode
endpoints:
- { host: update.code.visualstudio.com, port: 443 }
- { host: "*.vo.msecnd.net", port: 443 }
- { host: vscode.download.prss.microsoft.com, port: 443 }
- { host: marketplace.visualstudio.com, port: 443 }
- { host: "*.gallerycdn.vsassets.io", port: 443 }
binaries:
- { path: /usr/bin/curl }
- { path: /usr/bin/wget }
- { path: "/sandbox/.vscode-server/**" }
- { path: "/sandbox/.vscode-remote-containers/**" }
What the GitHub blocks do:
| Block | Endpoint | Access |
|---|---|---|
github_git |
github.com:443 |
Git Smart HTTP with TLS termination. Clone and fetch allowed for both repos. Push (git-receive-pack) allowed only for alpha-repo. All other repos are denied. |
github_api |
api.github.com:443 |
REST API with TLS termination. Full read-write for alpha-repo. Read-only (GET, HEAD, OPTIONS) for bravo-repo. All other repos are denied. |
The remaining blocks (claude_code, nvidia_inference, pypi, vscode) match the {doc}default policy </reference/default-policy> so the sandbox behaves the same as a standard sandbox for everything outside of GitHub.
For background on how network policy blocks work, see Network Access Rules.
Step 3: Create the Sandbox
Create the sandbox with both the GitHub provider and your policy:
$ nemoclaw sandbox create \
--provider my-github \
--policy github-policy.yaml \
--keep \
-- claude
The --keep flag keeps the sandbox running after Claude Code exits, so you can reconnect or iterate on the policy.
Step 4: Verify Access
Once Claude Code is running inside the sandbox, ask it to exercise both repos. The policy should allow writes to alpha-repo and block writes to bravo-repo.
Test read-write access — ask Claude to clone, commit, and push to alpha-repo:
Clone https://github.com/<org>/alpha-repo.git, add a blank line to the
README, commit, and push.
Claude clones the repo, makes the edit, and pushes. The sandbox logs show action=allow for both github.com (git push) and api.github.com (any API calls Claude makes along the way).
Test read-only enforcement — ask Claude to try the same thing with bravo-repo:
Clone https://github.com/<org>/bravo-repo.git, add a blank line to the
README, commit, and push.
Claude clones successfully (read is allowed), but the push fails. The proxy denies git-receive-pack for bravo-repo and Claude reports the error. You can confirm in the logs:
$ nemoclaw logs <sandbox-name> --tail --source sandbox
Look for an action=deny entry showing host=github.com and path=/<org>/bravo-repo.git/git-receive-pack.
Test API scoping — ask Claude to create an issue on each repo:
Create a GitHub issue titled "Test from sandbox" on <org>/alpha-repo.
Then try to create the same issue on <org>/bravo-repo.
The first issue is created. The second is denied — the policy only allows GET/HEAD/OPTIONS for bravo-repo, so the POST to create an issue is blocked.
Step 5: Iterate on the Policy
To grant access to additional repos or change access levels, edit github-policy.yaml and push the update to the running sandbox:
$ nemoclaw policy set <sandbox-name> --policy github-policy.yaml --wait
For example, to grant write access to bravo-repo as well, add another rule under github_api:
- allow:
method: "*"
path: "/repos/<org>/bravo-repo/**"
And add a push rule under github_git:
- allow:
method: POST
path: "/<org>/bravo-repo.git/git-receive-pack"
For the full iterate workflow (pull current policy, edit, push, verify), see {doc}/sandboxes/policies.
Next Steps
- Need other credentials? See {doc}
/sandboxes/providersfor all supported provider types. - Want finer policy control? See {doc}
/sandboxes/policiesfor more examples and the iterate workflow. - Looking for the full YAML reference? See the Policy Schema Reference.