> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/NVIDIA/OpenShell/llms.txt
> Use this file to discover all available pages before exploring further.

# Grant GitHub push access to a sandboxed agent

> Learn the iterative policy workflow by launching a sandbox, diagnosing a GitHub access denial, and applying a custom policy to grant scoped write access.

This tutorial walks through an iterative sandbox policy workflow. You launch a sandbox, ask Claude Code to push code to GitHub, and observe the default network policy denying the request. You then diagnose the denial from your machine and from inside the sandbox, apply a policy update, and verify that the update takes effect.

After completing this tutorial, you will have:

* A running sandbox with Claude Code that can push to a GitHub repository.
* A custom network policy that grants GitHub access for a specific repository.
* Experience with the policy iteration workflow: fail, diagnose, update, verify.

<Note>
  This tutorial shows example prompts and responses from Claude Code. The exact
  wording you see might vary between sessions. Use the examples as a guide for
  the type of interaction, not as expected output.
</Note>

## Prerequisites

* A working OpenShell installation. Complete the [Quickstart](/quickstart) before proceeding.
* A GitHub personal access token (PAT) with `repo` scope. Generate one from the [GitHub personal access token settings page](https://github.com/settings/tokens) by selecting **Generate new token (classic)** and enabling the `repo` scope.
* An [Anthropic account](https://console.anthropic.com/) with access to Claude Code. OpenShell provides the sandbox runtime, not the agent. You must authenticate with your own account.
* A GitHub repository you own to use as the push target. A scratch repository is sufficient. You can [create one](https://github.com/new) with a README if needed.

This tutorial uses two terminals to demonstrate the iterative policy workflow:

* **Terminal 1**: The sandbox terminal. You create the sandbox in this terminal by running `openshell sandbox create` and interact with Claude Code inside it.
* **Terminal 2**: A terminal outside the sandbox on your machine. You use this terminal for viewing sandbox logs with `openshell term` and applying an updated policy with `openshell policy set`.

## Tutorial

<Steps>
  <Step title="Set up a sandbox with your GitHub token">
    <Tabs>
      <Tab title="Starting a new sandbox">
        In Terminal 2, create a credential provider that injects your GitHub token into the sandbox automatically. The provider reads `GITHUB_TOKEN` from your host environment and sets it as an environment variable inside the sandbox.

        The [default policy](/reference/default-policy) is applied automatically, which allows read-only access to GitHub.

        ```console theme={null}
        $ GITHUB_TOKEN=<your-token>
        $ openshell provider create --name my-github --type github --from-existing
        $ openshell sandbox create --provider my-github -- claude
        ```

        `openshell sandbox create` keeps the sandbox running after Claude Code exits so you can apply policy updates without recreating the environment. Add `--no-keep` if you want the sandbox deleted automatically instead.

        Claude Code starts inside the sandbox. It prints an authentication link. Open it in your browser, sign in to your Anthropic account, and return to the terminal. When prompted, trust the `/sandbox` workspace to allow Claude Code to read and write files.
      </Tab>

      <Tab title="Using an existing sandbox">
        In Terminal 1, connect to a sandbox that is already running and set your GitHub token as an environment variable:

        ```console theme={null}
        $ openshell sandbox connect <sandbox-name>
        $ export GITHUB_TOKEN=<your-token>
        ```

        To find the name of running sandboxes, run `openshell sandbox list` in Terminal 2.
      </Tab>
    </Tabs>
  </Step>

  <Step title="Push code to GitHub">
    In Terminal 1, ask Claude Code to write a simple script and push it to your repository. Replace `<org>` with your GitHub organization or username and `<repo>` with your repository name.

    ```text theme={null}
    Write a hello_world.py script and push it to https://github.com/<org>/<repo>.
    ```

    Claude recognizes that it needs GitHub credentials. It asks how you want to authenticate. Provide your GitHub personal access token by pasting it into the conversation. Claude configures authentication and attempts the push.

    The push fails. Claude reports an error, but the failure is not an authentication problem. The default sandbox policy permits read-only access to GitHub and blocks write operations, so the proxy denies the push before the request reaches the GitHub server.
  </Step>

  <Step title="Diagnose the denial">
    ### View the logs from your machine

    In Terminal 2, launch the OpenShell terminal:

    ```console theme={null}
    $ openshell term
    ```

    The dashboard shows sandbox status and a live stream of policy decisions. Look for entries with `l7_decision=deny`. Select a deny entry to see the full detail:

    ```text theme={null}
    l7_action:      PUT
    l7_target:      /repos/<org>/<repo>/contents/hello_world.py
    l7_decision:    deny
    dst_host:       api.github.com
    dst_port:       443
    l7_protocol:    rest
    policy:         github_rest_api
    l7_deny_reason: PUT /repos/<org>/<repo>/contents/hello_world.py not permitted by policy
    ```

    The log shows that the sandbox proxy intercepted an outbound `PUT` request to `api.github.com` and denied it. The `github_rest_api` policy allows read operations (GET) but blocks write operations (PUT, POST, DELETE). A similar denial appears for `github.com` if Claude attempted a git push over HTTPS.

    ### Ask Claude Code to check the sandbox logs

    In Terminal 1, ask Claude Code to investigate:

    ```text theme={null}
    Check the sandbox logs for any denied network requests. What is blocking the push?
    ```

    Claude reads the deny entries and identifies the root cause. It explains that the failure is a sandbox network policy restriction, not a token permissions issue:

    > The sandbox runs a proxy that enforces policies on outbound traffic.
    > The `github_rest_api` policy allows GET requests (used to read the file)
    > but blocks PUT/write requests to GitHub. This is a sandbox-level restriction,
    > not a token issue. No matter what token you provide, pushes through the API
    > will be blocked until the policy is updated.

    Both perspectives confirm the same thing: the proxy is doing its job. The default policy is designed to be restrictive. To allow GitHub pushes, you need to update the network policy.

    Copy the deny reason from Claude's response. You paste it into an agent on your machine in the next step.
  </Step>

  <Step title="Generate an updated policy">
    In Terminal 2, paste the deny reason from the previous step into your coding agent on your machine (such as Claude Code or Cursor) and ask it to recommend a policy update. The deny reason gives the agent the context it needs to generate the correct policy rules.

    ```text theme={null}
    Based on the following deny reasons, recommend a sandbox policy update that
    allows GitHub pushes to https://github.com/<org>/<repo>, and save to
    /tmp/sandbox-policy-update.yaml:

    <paste deny reason here>

    The filesystem_policy, landlock, and process sections are static. They are
    read once at sandbox creation and cannot be changed by a hot-reload. Include
    them for completeness so the file is self-contained, but only the
    network_policies section takes effect when you apply this to a running sandbox.
    ```

    The agent will:

    1. Inspect the deny reasons.
    2. Write an updated policy that adds `github_git` and `github_api` blocks granting write access to your repository.
    3. Save the policy to `/tmp/sandbox-policy-update.yaml`.
  </Step>

  <Step title="Review the generated policy">
    Before applying, review the generated policy against the reference below. Confirm it grants only the access you expect — in this case, `git push` operations and GitHub REST API access scoped to a single repository.

    <Accordion title="Full reference policy">
      The following YAML shows a complete policy that extends the [default policy](/reference/default-policy) with GitHub access for a single repository. Replace `<org>` with your GitHub organization or username and `<repo>` with your repository name.

      The `filesystem_policy`, `landlock`, and `process` sections are static. They are read once at sandbox creation and cannot be changed by a hot-reload. They are included for completeness so the file is self-contained, but only the `network_policies` section takes effect when you apply this to a running sandbox.

      ```yaml theme={null}
      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 }
            - { 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
              enforcement: enforce
              rules:
                - allow:
                    method: GET
                    path: "/<org>/<repo>.git/info/refs*"
                - allow:
                    method: POST
                    path: "/<org>/<repo>.git/git-upload-pack"
                - allow:
                    method: POST
                    path: "/<org>/<repo>.git/git-receive-pack"
          binaries:
            - { path: /usr/bin/git }

        # GitHub: REST API
        github_api:
          name: github-api
          endpoints:
            - host: api.github.com
              port: 443
              protocol: rest
              enforcement: enforce
              rules:
                # GraphQL API (used by gh CLI)
                - allow:
                    method: POST
                    path: "/graphql"
                # Full read-write access to the repository
                - allow:
                    method: "*"
                    path: "/repos/<org>/<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: "/sandbox/.uv/python/**/python*" }
            - { 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/**" }
      ```

      The following table summarizes the two GitHub-specific blocks:

      | Block        | Endpoint             | Behavior                                                                                                                                                                                                |
      | ------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
      | `github_git` | `github.com:443`     | Git Smart HTTP protocol. Permits `info/refs` (clone/fetch), `git-upload-pack` (fetch data), and `git-receive-pack` (push) for the specified repository. Denies all operations on unlisted repositories. |
      | `github_api` | `api.github.com:443` | REST API. Permits all HTTP methods for the specified repository path and GraphQL queries. Denies API access to unlisted repositories.                                                                   |

      The remaining blocks (`claude_code`, `nvidia_inference`, `pypi`, `vscode`) are identical to the [default policy](/reference/default-policy). The default policy's `github_ssh_over_https` and `github_rest_api` blocks are replaced by `github_git` and `github_api`, which grant write access to the specified repository.
    </Accordion>
  </Step>

  <Step title="Apply the policy">
    After reviewing the generated policy, apply it to the running sandbox:

    ```console theme={null}
    $ openshell policy set <sandbox-name> --policy /tmp/sandbox-policy-update.yaml --wait
    ```

    Network policies are hot-reloadable. The `--wait` flag blocks until the policy engine confirms the new revision loaded. The update takes effect immediately without restarting the sandbox or reconnecting Claude Code.
  </Step>

  <Step title="Retry the push">
    In Terminal 1, ask Claude Code to retry:

    ```text theme={null}
    The sandbox policy has been updated. Try pushing to the repository again.
    ```

    The push completes successfully. The `openshell term` dashboard now shows `l7_decision=allow` entries for `api.github.com` and `github.com` where it previously showed denials.
  </Step>

  <Step title="Clean up">
    When you are finished, delete the sandbox to free cluster resources:

    ```console theme={null}
    $ openshell sandbox delete <sandbox-name>
    ```
  </Step>
</Steps>

## What's next

<CardGroup cols={2}>
  <Card title="Policy schema reference" icon="file-code" href="/reference/policy-schema">
    Add per-repository access levels, restrict to specific API methods, and explore all available policy fields.
  </Card>

  <Card title="Sandbox policies" icon="shield" href="/sandboxes/policies">
    Learn the full policy iteration workflow: pull, edit, push, verify.
  </Card>

  <Card title="Manage providers" icon="key" href="/sandboxes/providers">
    Inject credentials automatically instead of pasting tokens.
  </Card>

  <Card title="First network policy" icon="graduation-cap" href="/tutorials/first-network-policy">
    Go back to the basics: create a sandbox and apply an L7 read-only rule.
  </Card>
</CardGroup>
