Files
OpenShell/docs/tutorials/github-sandbox.md
T
2026-03-10 18:04:25 -07:00

13 KiB

title, description, topics, tags, content
title description topics tags content
page nav
Set Up a Sandbox with Custom GitHub Policies GitHub Sandbox Tutorial
Configure a sandbox with per-repository GitHub access policies and credential providers for Claude Code.
Generative AI
Cybersecurity
Tutorial
GitHub
Sandbox
Policy
Claude Code
Credential Provider
type difficulty audience
how_to technical_intermediate
engineer

Set Up a Sandbox of Claude Code with a Custom GitHub Policy

This tutorial walks through configuring a sandbox that grants different access levels to two GitHub repositories and enforces these rules at the network layer. The sandbox runs Claude Code as the agent.

The tutorial uses the following two example repositories for illustration purposes.

  • A alpha-repo repository with read-write access. The agent can clone, push, and call mutating GitHub API endpoints such as pull requests, issues, and comments.
  • A bravo-repo repository with read-only access. The agent can clone and fetch, but push operations and mutating API calls are denied.
  • All other GitHub repositories are denied by default. No clone, fetch, or API call to an unlisted repository is allowed.

After completing this tutorial, the sandbox environment includes the following:

  • A GitHub credential provider that injects your GitHub token into the sandbox at runtime.
  • A network policy that extends the default policy with per-repository GitHub access rules.
  • A running sandbox in which Claude Code operates under the defined access constraints.

Prerequisites

This tutorial requires the following:

  • Completed the {doc}Quickstart </get-started/quickstart> tutorial.
  • A GitHub personal access token (PAT) with repo scope, exported as GITHUB_TOKEN.
  • An agent API key configured in the environment. For example, ANTHROPIC_API_KEY for Claude Code.

Create a GitHub Provider

In this section, you learn how to create a GitHub provider that injects your token into the sandbox at runtime.

:::{admonition} Already have a sandbox running? :class: tip

If you completed the Quickstart tutorial and have the default sandbox without a GitHub provider, there are two options to add the provider.

  • Option 1: Recreate with a provider. Delete the existing sandbox, create the provider below, then recreate the sandbox with --provider my-github.
  • Option 2: Inject the token manually. Connect with openshell sandbox connect <name> and run export GITHUB_TOKEN=<your-token>. This bypasses the provider workflow. Note that the token does not persist across sandbox recreations. :::

Create a provider that sources your GitHub token from the host environment:

$ openshell provider create --name my-github --type github --from-existing

This command reads GITHUB_TOKEN (and GH_TOKEN if set) from the current shell session and stores them in the provider configuration. At sandbox startup, the provider injects these values as environment variables into the container.

For additional provider types, refer to {doc}/sandboxes/providers.

Write the Policy

In this section, you learn how to write a policy that extends the default policy with per-repository GitHub access rules.

Run the following command to create a file named github-policy.yaml with policy blocks. This policy extends the {doc}default policy </reference/default-policy> and overrides the GitHub network blocks with per-repository rules. The alpha-repo receives read-write access, the bravo-repo receives read-only access, and all other repositories are denied by default.

Replace every occurrence of <org> with your GitHub organization or username.

$ cat << 'EOF' > github-policy.yaml
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/**" }
EOF

The following table summarizes the behavior of the GitHub policy blocks.

Block Endpoint Behavior
github_git github.com:443 Git Smart HTTP protocol with TLS termination. Permits info/refs (clone/fetch) for both repositories. Permits git-receive-pack (push) for alpha-repo only. Denies all operations on unlisted repositories.
github_api api.github.com:443 REST API with TLS termination. Permits all HTTP methods for alpha-repo. Restricts bravo-repo to GET, HEAD, and OPTIONS (read-only). Denies API access to unlisted repositories.

The remaining blocks (claude_code, nvidia_inference, pypi, vscode) are identical to the {doc}default policy </reference/default-policy>. Sandbox behavior outside of GitHub operations is unchanged.

For details on network policy block structure, refer to Network Access Rules.

Create the Sandbox

Run the following command to create the sandbox with the GitHub provider, the custom policy applied, and Claude Code running inside the sandbox:

$ openshell sandbox create \
    --provider my-github \
    --policy github-policy.yaml \
    --keep \
    -- claude

The --keep flag keeps the sandbox running after Claude Code exits. With this flag, you can reconnect to the same sandbox or apply policy updates without recreating the environment.

Verify Access

With Claude Code running inside the sandbox, validate that the policy enforces the expected access levels for each repository.

Verify Read-Write Access

Instruct Claude to clone, commit, and push to alpha-repo:

Clone https://github.com/<org>/alpha-repo.git, add a blank line to the
README.md file, commit, and push.

The clone, commit, and push operations should complete successfully. Verify that the sandbox logs contain action=allow entries for github.com (the git push) and api.github.com (any associated API calls).

Verify Read-Only Enforcement

Instruct Claude to perform the same operations on bravo-repo:

Clone https://github.com/<org>/bravo-repo.git, add a blank line to the
README.md file, commit, and push.

The clone operation succeeds because the policy permits read access. The push operation fails because the proxy denies git-receive-pack for bravo-repo. Verify the denial by inspecting the sandbox logs:

$ openshell logs <sandbox-name> --tail --source sandbox

The output contains an action=deny entry with host=github.com and path=/<org>/bravo-repo.git/git-receive-pack.

Verify API Scoping

Instruct Claude to create a GitHub issue on each repository:

Create a GitHub issue titled "Test from sandbox" on <org>/alpha-repo.
Then try to create the same issue on <org>/bravo-repo.

Expected result is that the issue is created on alpha-repo. The request to bravo-repo is denied because the policy restricts that repository to GET, HEAD, and OPTIONS methods. The POST required to create an issue is blocked by the proxy.

Iterate on the Policy

In this section, you learn how to iterate on the policy to modify repository access or add new repositories.

Network policies support hot-reloading. To modify repository access or add new repositories, edit github-policy.yaml and run the following command to apply the updated policy to the running sandbox:

$ openshell policy set <sandbox-name> --policy github-policy.yaml --wait

For example, to change the access level of bravo-repo from read-only to read-write, add the following rule under github_api:

          - allow:
              method: "*"
              path: "/repos/<org>/bravo-repo/**"

Then add the corresponding push rule under github_git:

          - allow:
              method: POST
              path: "/<org>/bravo-repo.git/git-receive-pack"

For the complete policy iteration workflow (pull, edit, push, verify), refer to {doc}/sandboxes/policies.

Next Steps

The following resources cover related topics in greater depth:

  • To configure additional credential types, refer to {doc}/sandboxes/providers.
  • To iterate on policy configuration, refer to {doc}/sandboxes/policies.
  • To view the policy YAML specification, refer to the Policy Schema Reference.