Auth
Beginner6 min read
Overview
GitHub tools authenticates every call with a GitHub token, and the token's permissions decide what the call can do. A token that can't push can't change your code, whatever the model decides, so give the agent only the permissions its job needs.
The token can come from the environment, from your own code on every call, or from Vercel Connect, which mints short-lived tokens on demand so there's no secret to store.
Configure
Set token where you create the tools or the agent. Without it, the SDK reads GITHUB_TOKEN:
import { createGithubTools } from '@github-tools/sdk'
const tools = createGithubTools({
preset: 'issue-triage',
token: async (call) => getInstallationToken(call?.owner),
})
token | When it's read |
|---|---|
| omitted | GITHUB_TOKEN, when the tools are created |
| a string | Once, when the tools are created |
(call?) => Promise<string> | On every tool call |
A function runs on every tool call, so a short-lived token stays fresh. It receives the call: toolName, input after context defaults, and owner and repo when the tool targets a repository.
Use a fine-grained token
For local work and single-tenant agents, create a fine-grained personal access token limited to the agent's repositories. Start with read access, and add write permissions only for the writes you plan to allow.
Permissions per preset
The table shows what each preset needs to run all of its tools. Every token also needs Metadata (read). Read access is enough when you only allow the preset's reads.
Repository permissions
4 of 12 granted, 5 scopes
- Actions
- Administration
- Checks
- Code scanning alerts
- Commit statuses
- Contentscontents:readRead-only
- Dependabot alerts
- Discussions
- Issuesissues:readissues:writeRead and write
- Metadatametadata:readRead-only
- Pull requestspull_requests:readRead-only
- Secret scanning alerts
A Connect token requests the matching scopes, such as issues:write.
| Preset | Contents | Pull requests | Issues | Discussions | Actions | Checks, statuses | Other |
|---|---|---|---|---|---|---|---|
repo-explorer | read | read | read | read | read | read | Gists, for the gist tools |
code-review | read | write | read, Commit statuses (write) | ||||
issue-triage | read | read | write | ||||
ci-ops | read | read | write | read, Commit statuses (write) | |||
security-audit | read | read | write | read | read | Code scanning, Dependabot and secret scanning alerts (read) | |
release-manager | write | read | write | read | |||
discussion-moderator | read | read | write | write | |||
notification-inbox | read | read | read | read | Notifications | ||
pr-author | write | write | |||||
maintainer | write | write | write | write | write | read, Commit statuses (write) | Administration (write), the three alert permissions (read), Gists, Notifications |
Releases fall under Contents and reactions under Issues, so neither needs a permission of its own.
createCommitStatus is the only tool that needs Commit statuses (write). With a personal access token, only that tool fails without it. With Vercel Connect, the preset's token asks for statuses:write on every call, so a GitHub App without that permission can make every tool in the preset fail. getCollaboratorPermission needs only Metadata (read). updatePullRequestBranch needs Pull requests (write), and a GitHub App also needs Contents (write) on the pull request's head repository.
Vercel Connect
On Vercel, Vercel Connect replaces the stored token. A GitHub connector, a GitHub App that Vercel manages, is attached to your project, and your code asks it for a short-lived token scoped to the preset. There's no secret to store or rotate.
- 12:04:01.120Agent
listIssues({ owner: 'vercel', repo: 'ai' })
- 12:04:01.122SDK
Token request for vercel/ai
contents:readmetadata:readpull_requests:readissues:readissues:write
- 12:04:01.304Connect
Minted token Anew
vercel installation, repository ai
- 12:04:01.588GitHub
GET /​repos/​vercel/​ai/​issues200
- 12:04:06.410Agent
addLabels({ issueNumber: 42, labels: ['bug'] })
write, approved
- 12:04:06.412SDK
Token request for vercel/ai, same preset scopes
contents:readmetadata:readpull_requests:readissues:readissues:write
- 12:04:06.415Connect
Reused token Acached
same connector and parameters
- 12:04:06.702GitHub
POST /​repos/​vercel/​ai/​issues/​42/​labels200
Token A is short-lived and expires on its own. A later call gets a new one.
Create a GitHub connector
- Create a connector of type
githubfrom the Vercel dashboard, or withvercel connect. - Install its GitHub App on the organization or account the agent works on, and pick the repositories.
- Link the connector to the project that runs the agent. Deployments authenticate with their OIDC token. Locally, run
vercel link, thenvercel env pull. - Install
@vercel/connect, an optional peer dependency used only by@github-tools/sdk/connect:
pnpm add @vercel/connect
npm install @vercel/connect
yarn add @vercel/connect
bun add @vercel/connect
Use the connector
connectGithubTools takes the connector name and the usual tool options, and derives the token's scopes from preset:
import { connectGithubTools } from '@github-tools/sdk/connect'
import { generateText } from 'ai'
const { text } = await generateText({
model: 'anthropic/claude-opus-5.5',
tools: connectGithubTools('github/my-connector', { preset: 'code-review' }),
prompt: 'Summarize the open pull requests on my-org/my-repo.',
})
The token is minted inside each tool call, and Connect caches it per connector and parameters.
For createGithubTools or a single tool, connectGithubToken returns the token function. Pass it the same preset, since it derives its scopes on its own:
import { createGithubTools } from '@github-tools/sdk'
import { connectGithubToken } from '@github-tools/sdk/connect'
const tools = createGithubTools({
preset: 'ci-ops',
token: connectGithubToken('github/my-connector', { preset: 'ci-ops' }),
})
To scope the token to a hand-picked tool set instead, pass include and exclude. connectGithubScopesForPreset and its siblings return the same scope lists, see the API reference.
Leave a tool out of the token
A preset's token asks for the scopes of every tool in it, so the connector's GitHub App needs each of those permissions. code-review, ci-ops and maintainer include createCommitStatus, which needs Commit statuses (read and write). Add that permission to the App, or leave the tool out of the token.
connectGithubTools always asks for the whole preset's scopes. Build the token with connectGithubToken and exclude, and drop the tool from the tool set:
import { createGithubTools } from '@github-tools/sdk'
import { connectGithubToken } from '@github-tools/sdk/connect'
const token = connectGithubToken('github/my-connector', { preset: 'code-review', exclude: ['createCommitStatus'] })
const { createCommitStatus, ...tools } = createGithubTools({ preset: 'code-review', token })
On the eve extension, exclude: ['createCommitStatus'] removes the tool and its scope together.
Use one app on several accounts
When the App is installed on several accounts, each call that targets owner/repo gets a token from the installation that owns that repository. Calls without a repository target, such as search, use the connector's default installation.
To pin one installation or narrow the token, pass Connect parameters as connect:
const tools = connectGithubTools('github/my-connector', {
preset: 'issue-triage',
connect: {
installationId: 'inst_abc',
repositories: ['my-org/my-repo'],
},
})
installationId, authorizationDetails or repositories turn off the per-repository targeting, and scopes replaces the derived scopes. connect can also be a function of the call, (call) => params.
Per-user tokens
By default every caller acts as the App, with the project's access. When each user connects their own GitHub account, pass a user subject so calls act as that user:
const tools = connectGithubTools('github/my-connector', {
preset: 'issue-triage',
connect: { subject: { type: 'user', id: currentUserId } },
})
A user who hasn't connected GitHub gets CONNECT_USER_NOT_CONNECTED instead of falling back to the App.
Call getToken yourself
For full control over the Connect parameters, call getToken from @vercel/connect inside a token function:
import { createGithubTools } from '@github-tools/sdk'
import { getToken } from '@vercel/connect'
const tools = createGithubTools({
preset: 'code-review',
token: () => getToken('github/my-connector', {
subject: { type: 'app' },
scopes: ['contents:read', 'pull_requests:read'],
}),
})
Expired OIDC tokens
When VERCEL_OIDC_TOKEN is set, as after vercel env pull, the SDK passes it to Connect directly. This also works in sandboxes without a .vercel directory. That token isn't refreshed: once it expires, calls fail with OIDC_TOKEN_EXPIRED before any request. Run vercel env pull again. Deployments get a fresh token on their own.
FAQ
createGithubTools() throws TOKEN_REQUIRED. A token function that returns an empty string fails the same way, when the tool is called.forkRepository and createRepository need Administration (write). Only the maintainer preset includes them.FORBIDDEN, and the rest of the preset keeps working.preset or include, the token gets the scopes of all 97 tools, Administration (write) included.CONNECT_INSTALLATION_REQUIRED, and the message names the account.await getToken(...) would run at import or build time, where there's no OIDC token.connector to the eve extension instead. For per-user tokens, eve uses actAs: 'user' or 'caller'.