
NemoHermes で GitHub token を渡さずに private リポジトリを読ませてみた
はじめに
こんにちは、クラスメソッド製造ビジネステクノロジー部の森茂です。
AI エージェントに GitHub の private リポジトリを読ませたい。でも GitHub token をそのまま渡すのはなぁ……。
普段のセットアップを思い出してみると、.env に GITHUB_TOKEN=ghp_... と書いて agent に読ませる、というのが一番素直な流れですよね。Claude Code や Cursor のような agent 側のハーネスにも、.gitignore への自動追加や secret 検出、env 変数の渡し方を絞り込む仕組みが少しずつ整ってきていて、「うっかり commit」「うっかりログ出力」のような事故は前よりは起こりにくくなっています。
ただ、それでも raw token が host 上の disk にそのまま置かれていることは変わりません。agent process は中身を生で読めますし、tool 呼び出しの中で curl にそのまま乗せたり、reasoning の途中でうっかり画面に出してしまったり、というリスクは仕組み的にはゼロにできない構造ですよね。private repository の token は PR のマージや Workflow の発火まで含めた強い権限を持っていることも多いので、agent が自由に使える状態で disk に置いておくのは、やっぱり少し落ち着かないところがあります。
NemoHermes / NemoClaw には、この悩みに対する答えとして OpenShell の Providers v2 という仕組みが用意されています。GitHub token の実値は OpenShell 側が預かり、agent には placeholder と呼ばれる「実値の代わりになる目印の文字列」だけを見せる構成です。
この記事では、NemoHermes の sandbox から GitHub の private repository にある PR を読むまでの手順を、実機で詰まったポイントも含めて紹介します。
NemoHermes 自体の導入手順は、以前の記事にまとめています。本記事は NemoHermes が導入済みで、sandbox が起動している状態から始めます。
今回のゴール
NemoHermes の terminal tool から、private repository の PR 情報を取得します。ポイントは、GitHub token の実値が sandbox の中に一度も入らないことです。
全体の流れは以下のとおりです。
sandbox 内の Python が送り出すリクエストに付いているのは placeholder で、sandbox の外へ出るタイミングで OpenShell の egress proxy が placeholder を実 token に差し替えます。そのため、agent 側のログや script に raw token が出てくることはありません。
OpenShell provider が token を預かる仕組み
OpenShell provider は、AI agent や sandbox が外部サービスへアクセスするときに使う credential を管理する仕組みです。GitHub token や LLM の API key を provider として登録しておくと、sandbox に raw credential を置かずに済みます。
Providers v2 では、credential に加えて、その provider が使う endpoint や binary、network policy も provider 側にまとまりました。GitHub provider を sandbox に attach すると、GITHUB_TOKEN という credential が渡るだけでなく、api.github.com への通信許可と、それを使ってよい binary の制約も _provider_* という名前空間の policy として一緒に降ってきます。既存の sandbox 自身の policy はそのまま残り、両者が重なって最終的なアクセス可否が決まる仕組みです。後から policy を絞り込みたい場合は、この「2 層が重なる」前提を意識しておくと混乱しにくくなります。
NemoHermes から見ると、環境変数 GITHUB_TOKEN には openshell:resolve:env:..._GITHUB_TOKEN のような placeholder が入っています。token の実値ではなく、OpenShell が egress 時に解決するための目印です。社内 repository を AI agent に読ませるとき、この境界があるかどうかはかなり大きな違いですね。
なお、placeholder は raw token ではないものの、credential 解決用のハンドルではあります。この記事でも完全な値は載せず、openshell:resolve:env:..._GITHUB_TOKEN とマスクして扱います。手元で確認した範囲では、この placeholder の値は credential 名(ここでは GITHUB_TOKEN)に紐づいているようで、同じ名前で provider を作り直しても文字列は変わりませんでした。後から provider の構成を見直したくなったときに、agent 側へ書いた .env を書き直さなくて済むのは地味にありがたいですね。
前提条件
この記事では、以下の状態から始めます。sandbox 名は nemohermes-demo とします。
- NemoHermes が導入済みで、sandbox が起動している
- host 側で
openshellコマンドが使える - host 側で GitHub CLI(
gh)が private repository を読める認証状態にある
GitHub CLI の認証状態は host 側で確認しておきます。
gh auth status
GitHub provider を作成する
まず、GitHub token を OpenShell provider として登録します。--from-existing は環境変数 GITHUB_TOKEN に入っている値を読み取って登録するオプションで、ここでは gh auth token の出力をその場で渡しています。
GITHUB_TOKEN="$(gh auth token)" \
openshell provider create --name nemohermes-demo-github --type github --from-existing
ここで登録した token は OpenShell gateway 側に保持されます。sandbox 内のファイルや .env に書く必要はありません。
登録できたか確認します。
openshell provider list
NAME TYPE CREDENTIAL_KEYS CONFIG_KEYS
nemohermes-demo-github github 1 0
Providers v2 を有効化して sandbox に attach する
provider を作成しただけでは、既存の sandbox には紐づきません。Providers v2 を有効化したうえで、対象の sandbox に attach します。
openshell settings set --global --key providers_v2_enabled --value true
✓ Set global setting providers_v2_enabled=true (revision 1)
Providers v2 は、検証時点ではこのように設定で明示的に有効化する opt-in の機能です。今後のバージョンで挙動が変わる可能性がある点はご了承ください。
GitHub の provider profile が見えているかも確認しておきます。
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 が source control provider として見えています。対象の sandbox に attach します。
openshell sandbox provider attach nemohermes-demo nemohermes-demo-github
✓ Attached provider nemohermes-demo-github to sandbox nemohermes-demo
attach 済みの provider を確認します。
openshell sandbox provider list nemohermes-demo
NAME TYPE CREDENTIAL_KEYS CONFIG_KEYS
nemohermes-demo-github github 1 0
sandbox 内で placeholder を確認する
attach した credential が sandbox からどう見えるか確認します。
openshell sandbox exec -n nemohermes-demo -- \
sh -lc 'printf "%s\n" "$GITHUB_TOKEN"'
openshell:resolve:env:..._GITHUB_TOKEN
GITHUB_TOKEN に入っているのは token の実値ではなく placeholder です。公式ドキュメント上の形式は openshell:resolve:env:<KEY> で、手元では key の前に内部 ID らしき prefix が付いた値が見えました。いずれにせよ、raw token が sandbox に入っていないことを、まずここで確認できました。
Providers v2 と Hermes runtime で押さえておきたい 2 つの仕組み
placeholder が見えたので、ここからは GitHub API を実際に叩く準備に入ります。Providers v2 と Hermes runtime には、credential を守るための仕組みが 2 段で入っていて、それぞれを押さえておくと後の手順がすんなり通ります。
GitHub API へのアクセスは許可された binary に寄せる
sandbox 内から curl で GitHub API を叩こうとすると、認証以前に OpenShell の proxy で止まります。
curl: (56) CONNECT tunnel failed, response 403
これは credential のエラーではなく、GitHub API に到達する前の policy deny です。openshell term で blocked request の deny reason を見ると、api.github.com が curl に許可された endpoint に含まれていないことが分かります。手元の NemoHermes sandbox で GitHub policy が api.github.com へのアクセスを許可していた binary は、/usr/bin/git と /opt/hermes/.venv/bin/python の 2 つだけでした。
Providers v2 では「どの binary がその endpoint を使ってよいか」まで provider 側の policy で決まります。同じ token でも、それを利用できる実行ファイルが絞られている、という設計ですね。
Hermes は terminal tool の子プロセスから credential を剥がす
もう 1 つは、Hermes runtime 側の振る舞いです。openshell sandbox exec で外から起動した process には GITHUB_TOKEN の placeholder が入りますが、NemoHermes の terminal tool が動かす子プロセスでは GITHUB_TOKEN が空になります。
Hermes は terminal や execute_code といった tool の子プロセスを起動するとき、credential に該当する名前の環境変数を意図的に剥がす作りになっています。GITHUB_TOKEN は Skills Hub 用の credential として登録されているため、除外の対象です。~/.hermes/.env に GITHUB_TOKEN=... と書いても、terminal tool の中の os.environ["GITHUB_TOKEN"] では空になる、という挙動はここから来ています。
これは GHSA-rhgp-j443-p4rf への対応として入っていて、悪意ある skill が credential を子プロセス経由で吸い出すのを防ぐ役割を担っています。terminal.env_passthrough や skill frontmatter の required_environment_variables でも、credential 名に該当する変数は通せません。設定で外す類のものではなく、尊重すべき安全境界というわけですね。
ということは、placeholder であっても GITHUB_TOKEN という名前のままでは取り除かれてしまいます。ここで使うのが、次の _HERMES_FORCE_ prefix です。
_HERMES_FORCE_ prefix で標準名のまま渡す
Hermes には、この credential scrub を正規の手順で通り抜けるための escape hatch が用意されています。環境変数名に _HERMES_FORCE_ という prefix を付けておくと、tool の子プロセスを起動するときに prefix が外れ、元の名前で注入されます。_HERMES_FORCE_GITHUB_TOKEN を渡しておけば、子プロセスでは GITHUB_TOKEN として見える、という仕組みです。
これを使うと、agent 側のコードは標準的な GITHUB_TOKEN を前提にしたまま動かせます。GitHub 系のツールやサンプルが GITHUB_TOKEN を読む前提で書かれていても、変数名を変えずに済むのは助かりますね。
設定は、sandbox の Hermes が読む .env に 1 行足すだけです。値には raw token ではなく、先ほど openshell sandbox exec で確認した placeholder を入れます。
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
'
placeholder を GITHUB_TOKEN から読んで _HERMES_FORCE_GITHUB_TOKEN として書き込み、既存の同名行があれば置き換えています。raw token ではなく placeholder を書いている点が大事なところです。
.env は Hermes の起動時に読み込まれるため、書き換えたあとは新しい session を開くか、agent を再起動して反映させます。
# agent の terminal tool から実行させるイメージ
printenv GITHUB_TOKEN
出力に openshell:resolve:env: で始まる placeholder が返ってくれば成功です。
ここまで来ると、terminal tool の中の Python から os.environ["GITHUB_TOKEN"] で placeholder を読めるようになります。あとは、その placeholder を Bearer token に載せて GitHub API を叩くだけです。
PR 読み取り用の Python script を用意する
NemoHermes の terminal tool から実行する Python script を用意します。
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))
script の中では raw token を要求しません。placeholder らしくない値が来たときに警告を出すガードだけ入れています。
NemoHermes から実行する
NemoHermes の terminal tool で github_read_pr.py を配置して実行します。_HERMES_FORCE_GITHUB_TOKEN を .env に入れてあるので、GITHUB_TOKEN を明示的に渡す必要はありません。agent はふつうに Python を実行するだけです。
GH_REPO='your-org/your-private-repo' \
PR_NUMBER='12' \
/opt/hermes/.venv/bin/python github_read_pr.py
実測では、private repository の PR 情報を取得できました。
{
"status": "ok",
"number": 12,
"title": "PR のタイトル",
"state": "open",
"html_url": "https://github.com/your-org/your-private-repo/pull/12",
"private": true,
"changed_files": 224,
"additions": 3645,
"deletions": 2223
}
private: true の repository から PR のメタデータを取得できています。この間、sandbox 内の process が見ていたのは placeholder だけで、raw token は OpenShell gateway の外に出ていません。
NemoHermes 用の skill にしておく
この手順は繰り返し使いそうだったので、NemoHermes 用の skill としても切り出しました。skill にしておくと、agent が GitHub がらみの依頼を受けたときに「raw token を要求しない」「GitHub API は Python で叩く」という今回のルールを毎回思い出してくれます。
配置先の例です。
~/.hermes/skills/nvidia/nemohermes-github-provider/SKILL.md
SKILL.md の最小構成(クリックで展開)
---
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.
伝えているルールは 3 つだけです。GitHub token の実値を要求しないこと、placeholder を GITHUB_TOKEN として扱うこと、GitHub API は /opt/hermes/.venv/bin/python から呼ぶこと。
sandbox への配置は skill install で行う
host 側で作った skill ディレクトリを NemoHermes の sandbox へ持ち込むときは、ファイルを手動コピーするのではなく、NemoClaw CLI の skill install を使います。SKILL.md frontmatter の検証、サブディレクトリ構造を保った upload、install 後の反映処理までまとめて面倒を見てくれる形です。
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
skill は sandbox 内の /sandbox/.hermes/skills/ 配下に配置され、hermes skills list に local skill として現れます。新しい session を開けば、agent 側の skills_list tool からも認識されます。
まとめ
OpenShell provider が token を預かり、Hermes runtime は credential 名の env を子プロセスから剥がす。provider と runtime の 2 段で credential を扱うので、agent からは placeholder しか見えませんし、egress 時に OpenShell 側で実 token に解決されるため、agent 側のログや script に raw token が出てくることもありません。
今回は GitHub token を題材にしましたが、Providers v2 の枠組み自体は LLM API key や社内 SaaS の credential にもそのまま広げられます。openshell provider list-profiles で見たとおり、INFERENCE や AGENT 系の provider profile も最初から用意されていて、agent に raw credential を持たせない構成を 1 つ組んでしまえば、新しい外部サービスを足すときも同じ流れで増やしていけるのは助かるところですね。
個人的には、AI agent に社内 repository を読ませるときの最初の一歩としても、credential 管理を見直すときのテンプレートとしても、かなり扱いやすい構成かなと思っています。







