Worked guide

Connect one GitHub repository with a read-only token

Create a narrowly scoped credential, prove that a second private repository is inaccessible, and revoke it after the job.

You’ll finish with
A verified one-repository connection and a repeatable revocation check.
Before you start
GitHub access to two private test repositories, permission to create a fine-grained token, Python 3, and a dedicated account where your service policy permits it.

1. Define the allowed and denied targets

Use a low-risk test repository containing a synthetic README. Choose a second private repository that you can confirm exists as its owner, but that the agent’s token must not access. Testing a public repository would not prove this boundary: public metadata may be readable without any credential.

Write down the single task, such as “read this repository and draft a summary.” It needs read access, not issues, administration, workflow changes or write access. A dedicated agent or machine account helps keep unrelated personal access out of the setup; follow GitHub’s account and organization policies.

2. Create the fine-grained token

  1. In GitHub, open Settings → Developer settings → Personal access tokens → Fine-grained tokens → Generate new token.
  2. Choose the resource owner and a short expiry appropriate to the test.
  3. Under repository access, choose Only select repositories and select the allowed test repository only.
  4. Under repository permissions, set Contents: Read-only. Metadata read access is included; leave other permissions at their minimum.
  5. Complete any required organization approval. A pending token cannot prove the connection works.
  6. Copy the token once into your private credential setup, never into an agent message, screenshot, shell command argument or source file.

A fine-grained token cannot grant more access than its account has. Conversely, choosing “all repositories” creates a much wider credential than this example needs.

3. Run the positive and negative check

Save the following as check_github_access.py. It uses Python’s standard library, contacts only api.github.com, refuses redirects, and prints status codes rather than token values or repository contents.

import getpass, os, re, sys
import urllib.error, urllib.request

class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None

repos = sys.argv[1:]
if len(repos) != 2 or any(not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", r) for r in repos):
    raise SystemExit("Usage: python3 check_github_access.py owner/allowed owner/denied")
token = os.environ.get("GITHUB_READ_TOKEN")
if not token:
    if not sys.stdin.isatty():
        raise SystemExit("Missing GITHUB_READ_TOKEN; use a private interactive terminal for setup")
    token = getpass.getpass("Read-only token (hidden): ")
opener = urllib.request.build_opener(NoRedirect)
statuses = []
for repo in repos:
    req = urllib.request.Request("https://api.github.com/repos/" + repo,
        headers={"Authorization": "Bearer " + token,
                 "Accept": "application/vnd.github+json",
                 "X-GitHub-Api-Version": "2022-11-28",
                 "User-Agent": "Hermes-read-only-check"})
    try:
        with opener.open(req, timeout=20) as response:
            status = response.status
    except urllib.error.HTTPError as error:
        status = error.code
        error.close()
    statuses.append(status)
    print(repo + ": HTTP " + str(status))
raise SystemExit(0 if statuses[0] == 200 and statuses[1] in (403, 404) else 1)

Run this in your own private interactive terminal, substituting the two repository names:

python3 check_github_access.py your-org/allowed-test your-org/denied-private-test

Paste the token at the hidden prompt. Expected: allowed repository HTTP 200; denied private repository HTTP 403 or 404; exit status 0. A 404 can also mean a typo, so first confirm that the denied repository really exists using your owner account. Do not print request headers to troubleshoot.

If the allowed check is 403/404, check repository selection, organization approval and token expiry. A rate-limit 403 is not a meaningful denial test: wait for the limit to clear and repeat. A network or TLS exception is a failed check, not proof of denial.

4. Give the job this credential, not your main login

For an unattended Hermes job, store the token under the exact name GITHUB_READ_TOKEN in a dedicated Bitwarden Secrets Manager project using the native setup recipe. If the terminal tool needs it, explicitly allow only that task variable in the profile’s existing terminal settings:

terminal:
  env_passthrough:
    - GITHUB_READ_TOKEN

Restart the actual job runner after changing its secret or configuration. This makes the token available to terminal-tool code in that profile; the agent can use it and may be able to read or disclose it. The repository restriction and expiry at GitHub are the important limits. Do not forward the vault’s bootstrap token or a general personal credential. For a Docker or SSH terminal, also follow the backend’s separate forwarding rules.

The checker reads GITHUB_READ_TOKEN when present, so the same script can be used as a tool-side connection test without a prompt. It is a diagnostic, not a complete GitHub integration. Install only the actual GitHub tool or integration you intend to use, then verify its real requests with this token.

When OAuth is the better route

For a service with a supported OAuth integration, prefer its consent flow when it offers the exact account and scopes you need. Read the permissions screen, choose the agent’s account, and make one real read-only request. Do not assume an app’s “connect” button limits access to a single folder or repository. If a scope is broader than the job, use a narrower account or integration.

5. Revoke it and prove a fresh request fails

  1. In GitHub’s fine-grained token settings, revoke or delete the test token.
  2. Run the checker again with the old token in your private terminal. A fresh request to the formerly allowed private repository should return an authentication or authorization error, commonly 401 or 403.
  3. Remove the stored secret and its passthrough entry if the job is retired; stop and restart the runner so it no longer holds the old token.
  4. Delete the synthetic test repositories if they are no longer useful.

Deleting a token from a local file or vault does not revoke it at GitHub. Revoking one token also does not close unrelated browser sessions, OAuth grants or SSH keys; remove each route you actually enabled.

Sources and next steps

All practical guides