← All articles Integrations

Give your agent GitHub access, not your keys.

OBTO Team · Insights from the Glass Box

A GitHub personal access token with repo scope can force-push to main, delete a branch, rewrite history, and close every open pull request in the repository. That is the exact credential most tutorials tell you to paste into a config file when you want to connect GitHub to an AI agent. It works on the first try, which is precisely the problem.

The token works. The demo is green. And you have quietly handed a non-deterministic process the same keys your senior engineer carries, with none of the judgment your senior engineer brings. The good news: the safer way is not much more work, and it is the difference between an agent that can open a pull request and one that can do anything the token allows.

There are two ways to wire an agent to GitHub

The first is to hand the model a credential — a personal access token, a fine-grained token, or a GitHub App installation token — and let it call the GitHub API directly. It is fast to set up. It also means every permission that credential carries is now reachable by whatever the model decides to do next.

The second is to put a thin server between the agent and GitHub. That server exposes a few named verbs — open_pull_request, comment_on_issue, request_review — holds the token itself, and decides, on every call, whether to allow the action, refuse it, or ask a human. The model never sees the credential. It sees a menu.

The distinction isn't cosmetic. In the first design, the boundary is the model's good behavior on a given afternoon. In the second, the boundary is code you wrote and can reason about. Production is a game about the worst case, and only one of these two sets the worst case deliberately.

What the safe pattern actually looks like

1. Give it verbs, not the API

Don't expose "call the GitHub REST API." Expose the three or four things the agent genuinely needs: open a PR from a branch, leave a review comment, add a label, read an issue. Each becomes a named tool with a typed input. An agent that only holds open_pull_request cannot force-push to main — not because you asked it nicely, but because there is no verb for it to reach.

2. Keep the token on the server

The credential lives inside the server that implements those verbs. It is never in the prompt, never in the browser client, never echoed into a log line. The agent says "open a PR titled X from branch Y"; the server attaches the token and makes the call. If a transcript leaks, it leaks a request, not a key. That single rule — the model asks, the server holds — is the whole of keeping an agent's secrets out of its own hands.

3. Put a policy in front of the write

Reading an issue is safe. Merging to main is not. That decision cannot live in the prompt, because a prompt is a request the model can talk itself past. It belongs on the server, between the agent and the action: allow the safe verbs, refuse the dangerous ones, and require a human for the ones in between. The model's intent is an input to that decision, never the final word — the argument we make in full in guardrails that don't live in the prompt.

A refusal should leave something behind. Here is what one looks like when an agent asks to merge a PR that policy reserves for a human:

{
  "run_id": "run_7b3e",
  "tool": "merge_pull_request",
  "target": "acme/web#412 -> main",
  "decision": "refused",
  "reason": "policy: merge_to_default_branch requires human approval",
  "human_needed": true,
  "retry_safe": true
}

Nothing was written. The agent gets a clear no, you get a one-line answer to "what did it try to do?", and the pull request is still sitting there for a person to approve.

4. Write down every call

Every verb the agent invokes should leave a receipt: which tool, what arguments, allowed or refused, and what GitHub returned. When a strange PR shows up at 2am, the receipt is how you learn what happened without reconstructing it from memory. An agent with write access and no ledger is a black box that occasionally edits your codebase.

A verb, up close

Concretely, the tool you expose is small and boring on purpose — a typed input, a fixed set of fields, and no room to improvise:

open_pull_request(
  repo:  "acme/web",        // fixed allow-list, not free text
  head:  branch,            // the agent's working branch
  base:  "develop",         // never "main"
  title: string,
  body:  string
) -> { number, url }        // returns a PR, nothing more

The agent can fill in a title and a body. It cannot choose to target main, cannot point at a repo that isn't on the list, and cannot reach any GitHub call you didn't wrap. Everything it's allowed to do is visible in five lines, which is also everything you have to reason about.

Before you connect GitHub to an agent

Four checks, in order. Each one closes a gap the naked token leaves open.

  1. Scope the verbs. Write down every GitHub action the agent needs. Expose those as named tools and nothing else. The list is your real permission model.
  2. Keep the token server-side. The credential lives in the server behind the verbs. If it can appear in a prompt, a client, or a log, move it.
  3. Add a refuse rule. Decide per verb: allow, refuse, or require a human. Put irreversible actions — merge, delete, force-push — behind a person, and back it with branch protection so main can't be written directly.
  4. Read a refused call's receipt. Trigger a blocked action on purpose and confirm you can tell what the agent tried, from the log alone. If you can't, neither will you at 2am.

But GitHub already ships an MCP server

It does, and you should use it. GitHub's official remote MCP server handles OAuth, and Claude, Cursor, ChatGPT, and VS Code all ship GitHub connectors that speak to it — point an agent at one and it can read repos, manage issues, and open PRs with no glue code from you. Underneath it still runs on a token, a fine-grained PAT or a GitHub App, so the scoping advice above still holds: grant the narrowest repositories and permissions the job needs, and run it read-only or with a trimmed toolset wherever you can.

A connector gives you what's possible. It hands the model a broad GitHub toolset and trusts it to reach for the safe ones. What it doesn't give you is a decision at call time: it won't refuse this merge to main because your policy reserves that for a human, and it won't hand you a single receipt that spans GitHub and the database and the ticket system the same run touched. Permissions set what's possible; a policy sets what's allowed right now; a receipt records what happened. The connector covers the first. The other two are still yours.

Where OBTO fits

That governance layer is the part OBTO owns. Wrap the official GitHub MCP server, or a token directly — either way the agent's calls pass through a policy you control and land on the Glass Receipt by default, next to every other tool the run touched. It's the same pattern we use to wrap ServiceNow and to put an agent in front of Postgres: narrow verbs, credentials held server-side, a policy that can say no, and one audit trail across all of them. Describe it, ship it, own it — including the repo.

So the question to ask before you wire an agent to your codebase isn't "can the model be trusted with a token?" It's the more useful one: what are the three or four things this agent should be able to do to my repo — and can it reach anything else? Name the verbs. Everything past that list should be a door the agent can't open.

Frequently asked questions

How do I connect GitHub to an AI agent?

Put a thin server between the agent and GitHub. Instead of giving the model a token and the whole REST API, expose a few named verbs the agent actually needs — open a pull request, comment on an issue, add a label. The server holds the GitHub credential, decides per call whether to allow the action, and records every call. The agent gets a menu, not a master key.

Is it safe to give an AI agent a GitHub personal access token?

Not directly. A classic repo-scoped token can force-push to main, delete branches, and rewrite history, and a non-deterministic model holding it can do any of that on a bad day. Use a fine-grained token or a GitHub App to shrink what's possible, but keep the credential on a server the agent calls through, never in the prompt or the client.

What GitHub permissions should an AI agent have?

The fewest that let it do its job. Scope a fine-grained token to specific repositories and the exact permissions the task needs — often read on contents and write on pull requests, nothing more. Then narrow further at the tool layer: expose only the verbs you want, so permissions the token technically grants stay unreachable by the agent.

How do I stop an AI agent from pushing bad code to main?

Don't give it a verb that can. Let the agent open a pull request; keep merging behind a human or a required review. Enforce that on the server between the agent and GitHub — allow the safe actions, refuse the dangerous ones, require approval for the rest — and pair it with branch protection so main can't be written directly no matter what the agent attempts.

Should I use GitHub's official MCP server or build my own tools?

Use both. GitHub's official remote MCP server (or a GitHub connector in Claude, Cursor, or ChatGPT) is the fastest way to give an agent GitHub access, and it handles OAuth for you. But a connector sets what's possible; it won't refuse a specific write at call time or give you one receipt across all the tools a run touches. Add a policy-and-receipt layer on top for control and audit — they're complementary, not either-or.

Give your agent verbs, not credentials

Wrap GitHub as scoped MCP tools with the token server-side, a policy on every write, and a receipt for each call — on by default.

Get started

More from the OBTO blog