I tried to have NemoHermes read a private repository without passing a GitHub token

I tried to have NemoHermes read a private repository without passing a GitHub token

When accessing GitHub private repositories from NemoHermes, I don't want to place raw tokens in the sandbox. By using OpenShell Providers v2, you can achieve a configuration where the actual token value is kept on the host side and only a placeholder is shown to the agent. I will introduce the implementation key points and areas where I got stuck, along with the steps involved.
2026.06.17

This page has been translated by machine translation. View original

Introduction

Hello, I'm Morishige from Classmethod's Manufacturing Business Technology Department.

I want to let an AI agent read GitHub private repositories. But handing over the GitHub token directly feels a bit uncomfortable……

When I think back to my usual setup, the most straightforward flow is writing GITHUB_TOKEN=ghp_... in .env and having the agent read it. On the agent harness side, tools like Claude Code and Cursor have been gradually adding mechanisms such as automatic .gitignore additions, secret detection, and restrictions on how env variables are passed, making accidents like "accidental commit" or "accidental log output" somewhat less likely than before.

That said, the raw token still sits on disk on the host. The agent process can read it directly, and there's no way to structurally eliminate the risk of it being passed as-is to curl within a tool call, or accidentally appearing on screen during reasoning. Since tokens for private repositories often carry strong permissions including PR merges and workflow triggers, having them sitting on disk where an agent can freely access them is still a bit unsettling.

NemoHermes / NemoClaw provides a mechanism called OpenShell Providers v2 as an answer to this concern. The actual value of the GitHub token is held by OpenShell, and the agent only sees what's called a placeholder — a marker string that substitutes for the real value.

In this article, I'll walk through the steps to read a PR from a GitHub private repository from within a NemoHermes sandbox, including the points where I got stuck during hands-on testing.

The NemoHermes setup procedure itself is covered in a previous article. This article starts from a state where NemoHermes is already installed and the sandbox is running.

https://dev.classmethod.jp/articles/dgx-spark-nemohermes-openshell-hermes-agent/

Goal for This Article

We'll retrieve PR information from a private repository using NemoHermes's terminal tool. The key point is that the actual GitHub token value never enters the sandbox.

The overall flow is as follows.

The request sent out by Python inside the sandbox carries a placeholder, and at the moment it leaves the sandbox, OpenShell's egress proxy replaces the placeholder with the real token. This means raw tokens never appear in agent-side logs or scripts.

How the OpenShell Provider Holds the Token

The OpenShell provider is a mechanism for managing credentials used when AI agents or sandboxes access external services. By registering a GitHub token or LLM API key as a provider, you can avoid placing raw credentials in the sandbox.

In Providers v2, in addition to credentials, the endpoints, binaries, and network policies used by that provider are all consolidated on the provider side. When you attach a GitHub provider to a sandbox, not only is the GITHUB_TOKEN credential passed, but the permission to communicate with api.github.com and the constraints on which binaries may use it also come along as policies under the _provider_* namespace. The sandbox's own existing policies remain intact, and both layers overlap to determine the final access permissions. If you want to tighten policies later, keeping this "2-layer overlap" premise in mind will help avoid confusion.

From NemoHermes's perspective, the environment variable GITHUB_TOKEN contains a placeholder like openshell:resolve:env:..._GITHUB_TOKEN. This is not the actual token value, but a marker for OpenShell to resolve at egress time. Having this boundary in place when letting an AI agent read internal repositories makes quite a significant difference.

Note that while the placeholder is not a raw token, it is a handle for credential resolution. In this article as well, we won't show the full value, and will handle it masked as openshell:resolve:env:..._GITHUB_TOKEN. From what I verified locally, this placeholder value appears to be tied to the credential name (in this case GITHUB_TOKEN), and the string didn't change even when recreating the provider under the same name. It's a subtle but appreciated benefit that you don't need to rewrite the .env on the agent side when you want to revisit the provider configuration later.

Prerequisites

This article starts from the following state. The sandbox name is nemohermes-demo.

  • NemoHermes is installed and the sandbox is running
  • The openshell command is available on the host side
  • GitHub CLI (gh) on the host side is authenticated to read private repositories

Check the GitHub CLI authentication status on the host side.

gh auth status

Creating a GitHub Provider

First, register the GitHub token as an OpenShell provider. --from-existing is an option that reads the value stored in the environment variable GITHUB_TOKEN and registers it; here we pass the output of gh auth token directly.

GITHUB_TOKEN="$(gh auth token)" \
  openshell provider create --name nemohermes-demo-github --type github --from-existing

The token registered here is held on the OpenShell gateway side. You don't need to write it in any files or .env inside the sandbox.

Verify that it was registered.

openshell provider list
NAME                 TYPE     CREDENTIAL_KEYS   CONFIG_KEYS
nemohermes-demo-github   github   1                 0

Enabling Providers v2 and Attaching to the Sandbox

Simply creating a provider does not link it to an existing sandbox. Enable Providers v2 and then attach it to the target sandbox.

openshell settings set --global --key providers_v2_enabled --value true
✓ Set global setting providers_v2_enabled=true (revision 1)

At the time of testing, Providers v2 is an opt-in feature that must be explicitly enabled through this setting. Please note that the behavior may change in future versions.

Also confirm that the GitHub provider profile is visible.

openshell provider list-profiles
Available Provider Profiles:

  INFERENCE
    nvidia       NVIDIA                                     endpoints: 1  inference

  AGENT
    claude-code  Claude Code                                endpoints: 3  inference

  SOURCE CONTROL
    github       GitHub                                     endpoints: 2

GitHub is visible as a source control provider. Attach it to the target sandbox.

openshell sandbox provider attach nemohermes-demo nemohermes-demo-github
✓ Attached provider nemohermes-demo-github to sandbox nemohermes-demo

Verify the attached providers.

openshell sandbox provider list nemohermes-demo
NAME                 TYPE     CREDENTIAL_KEYS   CONFIG_KEYS
nemohermes-demo-github   github   1                 0

Verifying the Placeholder Inside the Sandbox

Let's check how the attached credential appears from within the sandbox.

openshell sandbox exec -n nemohermes-demo -- \
  sh -lc 'printf "%s\n" "$GITHUB_TOKEN"'
openshell:resolve:env:..._GITHUB_TOKEN

What's in GITHUB_TOKEN is not the actual token value but a placeholder. The format in the official documentation is openshell:resolve:env:<KEY>, and locally I saw a value with what appears to be an internal ID prefix before the key. Either way, we've confirmed here that the raw token is not inside the sandbox.

Two Mechanisms to Understand in Providers v2 and Hermes Runtime

Now that we've seen the placeholder, let's prepare to actually call the GitHub API. Providers v2 and the Hermes runtime have a two-layer mechanism to protect credentials, and understanding each one will make the subsequent steps go smoothly.

Route GitHub API Access Through Permitted Binaries

If you try to call the GitHub API with curl from within the sandbox, it gets blocked by the OpenShell proxy before even reaching authentication.

curl: (56) CONNECT tunnel failed, response 403

This is not a credential error — it's a policy deny before reaching the GitHub API. Looking at the deny reason for the blocked request in openshell term, you can see that api.github.com is not included among the endpoints permitted for curl. In the NemoHermes sandbox I tested, the only two binaries for which the GitHub policy permitted access to api.github.com were /usr/bin/git and /opt/hermes/.venv/bin/python.

In Providers v2, even which binary is allowed to use that endpoint is determined by the provider's policy. Even with the same token, the executables that can use it are restricted — that's the design.

Hermes Strips Credentials from Child Processes of the Terminal Tool

The other aspect is the behavior of the Hermes runtime. Processes launched externally via openshell sandbox exec have the GITHUB_TOKEN placeholder, but in child processes run by NemoHermes's terminal tool, GITHUB_TOKEN is empty.

When Hermes launches child processes for tools like terminal or execute_code, it intentionally strips environment variables whose names correspond to credentials. GITHUB_TOKEN is registered as a credential for Skills Hub, so it's subject to exclusion. This is why writing GITHUB_TOKEN=... in ~/.hermes/.env results in os.environ["GITHUB_TOKEN"] being empty inside a terminal tool — that behavior comes from here.

This was introduced as a response to GHSA-rhgp-j443-p4rf, and it plays a role in preventing malicious skills from extracting credentials via child processes. Even with terminal.env_passthrough or required_environment_variables in skill frontmatter, variables corresponding to credential names cannot be passed through. This is not something to be disabled by configuration — it's a safety boundary to be respected.

That means even a placeholder will be stripped if it retains the name GITHUB_TOKEN. This is where the _HERMES_FORCE_ prefix comes in.

Passing Under the Standard Name with the _HERMES_FORCE_ Prefix

Hermes provides an escape hatch to legitimately pass through this credential scrubbing. If you add a prefix of _HERMES_FORCE_ to an environment variable name, the prefix is removed when launching a child process for a tool, and it's injected under the original name. If you pass _HERMES_FORCE_GITHUB_TOKEN, it will appear as GITHUB_TOKEN in the child process.

Using this, the agent-side code can run with the standard GITHUB_TOKEN assumption intact. It's helpful to not have to change variable names even when GitHub-related tools and samples are written to read GITHUB_TOKEN.

The configuration is just adding one line to the .env read by Hermes in the sandbox. For the value, use the placeholder confirmed earlier with openshell sandbox exec, not the raw token.

openshell sandbox exec -n nemohermes-demo -- sh -lc '
  ph="$GITHUB_TOKEN"
  grep -v "^_HERMES_FORCE_GITHUB_TOKEN=" /sandbox/.hermes/.env 2>/dev/null > /sandbox/.hermes/.env.tmp || true
  printf "_HERMES_FORCE_GITHUB_TOKEN=%s\n" "$ph" >> /sandbox/.hermes/.env.tmp
  mv /sandbox/.hermes/.env.tmp /sandbox/.hermes/.env
'

This reads the placeholder from GITHUB_TOKEN and writes it as _HERMES_FORCE_GITHUB_TOKEN, replacing any existing line with the same name. The important point is that we're writing the placeholder, not the raw token.

Since .env is read when Hermes starts, after making this change, open a new session or restart the agent to apply it.

# Image of running from the agent's terminal tool
printenv GITHUB_TOKEN

If the output returns a placeholder starting with openshell:resolve:env:, you've succeeded.

At this point, Python inside the terminal tool can read the placeholder via os.environ["GITHUB_TOKEN"]. All that's left is to put that placeholder in a Bearer token and call the GitHub API.

Preparing the Python Script for Reading PRs

Let's prepare the Python script to run from NemoHermes's terminal tool.

github_read_pr.py
import json
import os
import sys
import urllib.request

repo = os.environ.get("GH_REPO", "owner/repo")
number = os.environ.get("PR_NUMBER", "1")
token = os.environ.get("GITHUB_TOKEN")

if not token:
    print("ERROR: GITHUB_TOKEN is missing from this process environment.")
    print("Expected an OpenShell placeholder such as openshell:resolve:env:..._GITHUB_TOKEN")
    sys.exit(2)

if not token.startswith("openshell:resolve:env:"):
    print("WARNING: GITHUB_TOKEN does not look like an OpenShell placeholder.")
    print("Do not continue if this is a raw token in a demo/logging context.")

url = f"https://api.github.com/repos/{repo}/pulls/{number}"
req = urllib.request.Request(
    url,
    headers={
        "Accept": "application/vnd.github+json",
        "Authorization": "Bearer " + token,
        "X-GitHub-Api-Version": "2022-11-28",
        "User-Agent": "nemohermes-demo",
    },
)

with urllib.request.urlopen(req, timeout=20) as resp:
    data = json.loads(resp.read().decode())

summary = {
    "status": "ok",
    "number": data.get("number"),
    "title": data.get("title"),
    "state": data.get("state"),
    "html_url": data.get("html_url"),
    "private": data.get("head", {}).get("repo", {}).get("private"),
    "changed_files": data.get("changed_files"),
    "additions": data.get("additions"),
    "deletions": data.get("deletions"),
}

print(json.dumps(summary, ensure_ascii=False, indent=2))

The script doesn't request a raw token. It only includes a guard that outputs a warning when a value that doesn't look like a placeholder is received.

Running from NemoHermes

Place github_read_pr.py and run it using NemoHermes's terminal tool. Since _HERMES_FORCE_GITHUB_TOKEN is in .env, you don't need to explicitly pass GITHUB_TOKEN. The agent simply runs Python normally.

GH_REPO='your-org/your-private-repo' \
PR_NUMBER='12' \
/opt/hermes/.venv/bin/python github_read_pr.py

In actual testing, I was able to retrieve PR information from a private repository.

{
  "status": "ok",
  "number": 12,
  "title": "PR title",
  "state": "open",
  "html_url": "https://github.com/your-org/your-private-repo/pull/12",
  "private": true,
  "changed_files": 224,
  "additions": 3645,
  "deletions": 2223
}

PR metadata was retrieved from a private: true repository. Throughout this process, all the sandbox processes ever saw was the placeholder, and the raw token never left the OpenShell gateway.

Turning It Into a NemoHermes Skill

Since I expected to use this procedure repeatedly, I also extracted it as a NemoHermes skill. By making it a skill, whenever the agent receives a GitHub-related request, it will remember the rules from this session: "don't request a raw token" and "call the GitHub API using Python."

Example placement location.

~/.hermes/skills/nvidia/nemohermes-github-provider/SKILL.md
Minimal SKILL.md structure (click to expand)
---
name: nemohermes-github-provider
description: Use when NemoHermes needs to read GitHub Issues or Pull Requests through OpenShell Providers v2 without exposing raw GitHub tokens.
version: 1.0.0
license: MIT
metadata:
  hermes:
    tags: [nemohermes, openshell, github, providers-v2, credentials]
---

# NemoHermes GitHub Provider

## Overview

Use this skill when NemoHermes needs to read GitHub Issues or Pull Requests from a private repository through OpenShell Providers v2.

The agent must not ask for a raw GitHub token. Use the OpenShell placeholder as `GITHUB_TOKEN` and call the GitHub REST API with `/opt/hermes/.venv/bin/python`.

Expected placeholder shape:

```text
openshell:resolve:env:..._GITHUB_TOKEN
```

## Rules

- Do not ask the user for a raw GitHub token.
- Do not print or save raw credentials.
- Treat `GITHUB_TOKEN` as an OpenShell placeholder.
- Do not use `curl` for GitHub API calls in this sandbox.
- Use `/opt/hermes/.venv/bin/python` for GitHub REST API calls.
- If `GITHUB_TOKEN` is missing, ask for the OpenShell placeholder, not the raw token.

## Read a pull request

Create `github_read_pr.py` and run it with `GH_REPO` and `PR_NUMBER`. `GITHUB_TOKEN` is injected into the process environment via the host-side `_HERMES_FORCE_GITHUB_TOKEN` setting, so do not pass it explicitly.

```bash
GH_REPO='owner/repo' \
PR_NUMBER='1' \
/opt/hermes/.venv/bin/python github_read_pr.py
```

The Python script should read `GITHUB_TOKEN` from the environment, use `Authorization: Bearer $GITHUB_TOKEN`, and summarize only task-relevant fields such as title, state, URL, changed file count, additions, deletions, and body.

## Troubleshooting

If `GITHUB_TOKEN` is missing from the process environment, stop and report it. Do not request the raw token, and do not read it from `/proc`. The fix is on the host side: set `_HERMES_FORCE_GITHUB_TOKEN=<placeholder>` in `/sandbox/.hermes/.env` and restart the agent so the placeholder is injected under the standard name.

If `curl` returns a proxy or policy 403, retry with `/opt/hermes/.venv/bin/python` instead of widening policy.

There are just three rules being conveyed. Don't request the actual GitHub token value, treat the placeholder as GITHUB_TOKEN, and call the GitHub API from /opt/hermes/.venv/bin/python.

Deploy to the Sandbox Using skill install

When bringing a skill directory created on the host side into the NemoHermes sandbox, use the NemoClaw CLI's skill install rather than manually copying files. It handles SKILL.md frontmatter validation, upload while preserving the subdirectory structure, and post-install processing all at once.

nemohermes nemohermes-demo skill install ./skills/nemohermes-github-provider
✓ Validated SKILL.md (name: nemohermes-github-provider, 3 files)
✓ Uploaded 3 file(s) to sandbox
Restart the agent gateway to pick up the new skill.
✓ Skill 'nemohermes-github-provider' installed

The skill is placed under /sandbox/.hermes/skills/ inside the sandbox and appears as a local skill in hermes skills list. Opening a new session makes it recognizable from the agent-side skills_list tool as well.

Summary

The OpenShell provider holds the token, and the Hermes runtime strips env variables with credential names from child processes. With credentials handled in these two layers, the agent only ever sees a placeholder, and since the actual token is resolved on the OpenShell side at egress time, raw tokens never appear in agent-side logs or scripts.

This time we used a GitHub token as the example, but the Providers v2 framework itself can be extended as-is to LLM API keys and internal SaaS credentials. As seen with openshell provider list-profiles, INFERENCE and AGENT provider profiles are available right from the start, and once you set up a configuration that doesn't give raw credentials to the agent, you can add new external services using the same flow — that's a genuinely helpful aspect.

Personally, I think this is quite a manageable setup both as a first step when letting an AI agent read internal repositories, and as a template for revisiting credential management.


AI白書2026 配布中

クラスメソッドが独自に行なったAI診断調査をもとに、企業のAI活用の現在地を調査レポートとしてまとめました。企業規模別の活用度傾向に加え、規模を超えてAI活用を進める企業に共通する取り組みまで、自社の現在地を捉えるためのヒントにぜひ。

AI白書2026

無料でダウンロードする

Share this article

DevelopersIO 2026