---
title: "How to prevent data loss on GitHub when working with agents"
date: 2026-10-01
canonical: https://solmaz.io/github-agent-token
license: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
---

*Note: This post is AI-assisted. It was written with Claude Opus 5.5 through Claude Code.*

My coding agents use the GitHub CLI (`gh`) to push code and merge pull requests. The token behind `gh` decides what an agent can do on GitHub, and the default one can also edit or switch off the branch rules that protect a repository. This post explains why that is a data loss risk, and how to set up a fine-grained personal access token that does the daily work but cannot change the rules.

## The default gh token

When you run `gh auth login` through the browser, `gh` receives an OAuth token that starts with `gho_` and carries the scopes `repo`, `read:org`, `workflow` and `gist`. The `repo` scope gives full control over every repository you administer, and that includes rulesets and repository settings. Classic OAuth scopes have no setting that removes only the admin part, so as long as `gh` runs on this token, so does an agent with full admin rights.

The dedicated `gh ruleset` command is read-only, which makes this easy to miss. The `gh api` command passes any request through to the REST API, though, so a single `gh api -X PUT repos/OWNER/REPO/rulesets/ID` call can rewrite or disable a ruleset with the token you already have.

## Rulesets as the last line against data loss

A force push to `main` replaces its history with whatever the agent has locally. Deleting a branch removes it entirely. In both cases the old commits stop being reachable from any branch, and getting them back means finding the old commit hashes in a local clone or asking GitHub support. Agents often work in fresh clones and worktrees that they delete afterwards, so the local copy you would rely on may not exist.

A ruleset that blocks force pushes and deletion on the default branch closes this hole, but only against someone who cannot edit the ruleset. A token that can edit rulesets turns them into advice. An agent that hits the block while it tries to rewrite history will often read the ruleset, disable it, force-push, and enable it again, because in the middle of a task it always has a good reason. The block has to hold anyway. The same Administration rights also let a token change the default branch. With a fine-grained token, they even allow deleting the repository outright.

When the token has no admin write access, the ruleset becomes a hard limit. The agent gets a 403, stops, and has to ask me. I then do the risky step myself with a full token, knowing what it does. Instructions in a prompt such as "never force-push" are text that a model can argue its way around in the middle of a task. A permission that GitHub enforces on the server cannot be argued with.

## Creating the token

Fine-grained tokens can only be created in the browser, at [github.com/settings/personal-access-tokens/new](https://github.com/settings/personal-access-tokens/new). There is no API to mint them, which I think is deliberate, since it keeps a human in the loop for every new token. After you pick the resource owner and the repositories, grant the permissions below.

These are the permissions I grant, with the reason for each one:

| Permission | Access | Reason |
|---|---|---|
| Contents | Read and write | Push and pull code |
| Pull requests | Read and write | Open and merge pull requests |
| Issues | Read and write | Issue work from `gh` |
| Workflows | Read and write | Push changes to files in `.github/workflows` |
| Actions | Read and write | Read CI logs and rerun jobs |
| Commit statuses | Read and write | Report check results |
| Secrets | Read and write | Set CI secrets (see below) |
| Administration | Read-only | Read rulesets to explain a rejected push, without editing them |
| Metadata | Read-only | Required by GitHub |
| Gists (account) | Read and write | Share snippets |

Everything else stays at "No access". Leave Administration write off above all, because it is the permission that edits rulesets. Webhooks deserve special mention, since a token with webhook write access can send every event in a repository to a server outside GitHub without leaving a diff anywhere.

Secrets is the judgment call in this list. The API never returns secret values. A token with write access can still replace one silently, and the next workflow run will use whatever the token put there. I set CI secrets through agents often enough that I keep this permission. If you don't, set it to "No access" and do those changes by hand.

It is tempting to grant everything except Administration and be done with it. I recommend against that, because the other permissions carry their own risks, and a daily token should only hold what the daily work needs. When a task needs more, grant it to the token for that task and take it away again, or do the task yourself.

## Installing the token

Give the token to `gh` without putting it in your shell history or a chat window. Run the following and paste the token when it asks:

```sh
gh auth login -h github.com --with-token
gh auth setup-git
```

The second command makes Git use the same token for `git push`. On macOS, check Keychain Access for an older `github.com` internet password entry. Git may still use that entry for pushes instead of the `gh` token, so delete it. On a Linux machine without a keyring, `gh` stores the token in plain text in `~/.config/gh/hosts.yml`, so set that file to mode 600.

## Checking what a token can do

The permission screen is easy to misread, so check the token against the API instead. For reads, send a GET request; 200 means the permission is there and 403 means it is not. For writes, send an empty body to a create endpoint, or a PUT to a made-up name. A 403 means the permission is missing. A 422 or 400 means GitHub accepted the permission and rejected the empty request during validation, so nothing changes.

```sh
gh api repos/OWNER/REPO/rulesets                          # 200: Administration read
gh api -X POST repos/OWNER/REPO/rulesets                  # 403: no Administration write
gh api -X POST repos/OWNER/REPO/git/refs                  # 422: Contents write
gh api -X PUT repos/OWNER/REPO/actions/secrets/__probe__  # 422: Secrets write
gh api repos/OWNER/REPO/hooks                             # 403: no Webhooks access
```

The second line is the one that matters for this post. If it returns 403, the token cannot edit or disable your rulesets.

## Limits of a token

Contents write still lets the agent force-push to and delete any branch that no ruleset covers. The token makes the rulesets hold, so the rulesets have to cover the branches you care about. I apply a default-branch ruleset to every new repository with [github-sane-defaults](https://github.com/osolmaz/github-sane-defaults).

A token is also a set of checkboxes. It can say "this token may push", but it cannot say "allow normal pushes, deny force pushes to `main`, and ask me before merging". For rules like that I am building [unYOLO](https://github.com/osolmaz/unyolo), a broker that holds the GitHub credential itself and gives the agent only a broker credential. Its policy file denies force pushes to `main` and ref deletion outright, and it sends a request to my phone before admin merges or repository deletion. Until that runs everywhere, the fine-grained token is the cheapest protection I have found, and it took about ten minutes to set up.
