How to Connect Cursor to GitHub (with OAuth)
๐ค AI Agent? You can point your AI agent at this article and it can follow the steps to connect automatically. Everything it needs is here: the API endpoints, request/response formats, scopes, and working code samples. The TapAuth API is two calls โ
POST https://tapauth.ai/api/v1/grantsto create a grant, thenGET https://tapauth.ai/api/v1/token/{id}(with Bearer auth) to retrieve the access token. No API key needed โ your agent can follow the code examples below end-to-end.
Cursor is a powerful AI-native code editor that understands your codebase and writes code alongside you. But when Cursor needs to interact with GitHub โ creating PRs, reading issues, managing branches โ it needs authenticated access to the GitHub API.
That means OAuth. And OAuth was designed for a world of web apps and browser redirects โ not AI agents running in editors and terminals.
The real problem: every agent rebuilds the same thing
If you're building an AI agent that talks to GitHub, you're about to discover something frustrating: most of your development time goes into authentication, not into the agent itself.
GitHub's OAuth has multiple flows, multiple token types, and a set of quirks that are unique to GitHub. Here's what you're signing up for:
- The device flow is the only sane option for agents โ and it's painful. The standard OAuth web flow requires a redirect URI and a browser. Agents don't have those. So you use the device flow: your agent gets a user code, displays it, and then polls GitHub in a loop waiting for the user to authorize. You need to handle
slow_downresponses,expired_tokenerrors, and interval backoff โ a surprisingly tricky state machine for what should be a one-time setup. - GitHub has four different token types. Classic personal access tokens, fine-grained personal access tokens, OAuth app tokens, and GitHub App installation tokens. Each has different scopes, expiration rules, rate limits, and API behaviors. Choosing wrong means refactoring later.
- Refresh behavior depends on token type. Classic OAuth tokens don't expire (but can be revoked). GitHub App tokens expire in 8 hours and need refresh. Your agent needs to know which type it has and handle each differently.
- Scopes are coarse. The
reposcope grants full read/write to every repository. Want read-only access to one repo? You need fine-grained tokens, which use a completely different authorization model. There's no simple "read this repo" scope in classic OAuth. - Org-level gates can silently break everything. Even with a valid token, organizations can require additional approval. Your agent gets a token, makes an API call, gets a 403 โ and the error message doesn't clearly explain that an org admin needs to approve the OAuth app. This is a support nightmare.
Now multiply this by every service your agent needs. GitHub today, Gmail tomorrow, Slack next week. Each provider has its own OAuth implementation, its own quirks, its own failure modes. You're not building an agent โ you're building an OAuth client that happens to also be an agent.
What the manual flow actually looks like
Here's the GitHub device flow in TypeScript. This is already simplified โ production code needs encrypted token storage, error handling for every edge case, and rate limit awareness.
// Step 1: Request device and user codes
const deviceResponse = await fetch(
'https://github.com/login/device/code',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
client_id: process.env.GITHUB_CLIENT_ID,
scope: 'repo read:org',
}),
}
);
const {
device_code,
user_code,
verification_uri,
interval,
} = await deviceResponse.json();
// Step 2: Tell the user to authorize
console.log(`Go to ${verification_uri} and enter code: ${user_code}`);
// Step 3: Poll for the token โ a surprisingly complex state machine
async function pollForToken(
deviceCode: string,
intervalSec: number
): Promise<string> {
while (true) {
await new Promise((r) => setTimeout(r, intervalSec * 1000));
const tokenResponse = await fetch(
'https://github.com/login/oauth/access_token',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
client_id: process.env.GITHUB_CLIENT_ID,
device_code: deviceCode,
grant_type:
'urn:ietf:params:oauth:grant-type:device_code',
}),
}
);
const data = await tokenResponse.json();
if (data.access_token) return data.access_token;
if (data.error === 'slow_down') {
intervalSec = data.interval; // GitHub wants us to back off
continue;
}
if (data.error === 'authorization_pending') continue;
if (data.error === 'expired_token') {
throw new Error('Device code expired โ restart the flow');
}
throw new Error(`OAuth error: ${data.error}`);
}
}
const accessToken = await pollForToken(device_code, interval);
// Step 4: Now you need to store this securely, handle refresh
// for GitHub App tokens, check rate limits, validate scopes...
// and THEN you can finally do the thing you actually wanted:
const repos = await fetch('https://api.github.com/user/repos', {
headers: {
Authorization: `Bearer ${accessToken}`,
'X-GitHub-Api-Version': '2022-11-28',
},
});
That's a polling loop, error handling for multiple states, and you still haven't dealt with token persistence, encryption, refresh, or rate limiting. This code has nothing to do with what your agent actually does โ it's pure infrastructure plumbing.
What if your agent could just ask for a token?
TapAuth is an access gateway for AI agents. The idea is simple: your agent shouldn't be an OAuth client. It should ask for a token and get one. TapAuth handles the entire OAuth dance โ the device flow, the consent screen, token exchange, encrypted storage, automatic refresh โ and gives your agent a working token through one API call.
// Step 1: Create a grant โ tell TapAuth what you need
const grantResponse = await fetch(
'https://tapauth.ai/api/v1/grants',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.TAPAUTH_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
provider: 'github',
scopes: ['repo', 'read:org'],
}),
}
);
const { grant_id, grant_secret, approve_url } = await grantResponse.json();
// Step 2: The user approves at the approve_url
// (Show this link to the user โ they authorize once)
console.log(`Approve access: ${approve_url}`);
// Step 3: Retrieve the token (poll until approved)
const tokenResponse = await fetch(
`https://tapauth.ai/api/v1/token/${grant_id}`,
{
headers: { 'Authorization': `Bearer ${grant_secret}` },
}
);
const { access_token } = await tokenResponse.json();
// Now do the thing you actually care about
const response = await fetch('https://api.github.com/user/repos', {
headers: {
Authorization: `Bearer ${access_token}`,
'X-GitHub-Api-Version': '2022-11-28',
},
});
const repos = await response.json();
No device flow polling loop. No token storage. No refresh logic. No "which token type am I dealing with" conditionals.
If the user hasn't approved yet, the token endpoint returns a pending status โ your agent just polls until it's ready. After one authorization, TapAuth handles refresh automatically. And when you need Gmail access next week, it's the same API โ just change the provider name. That's the point: TapAuth is the token layer for AI agents. One API to connect any OAuth provider.
What you can build with Cursor + GitHub + TapAuth
With TapAuth handling authentication, you can focus on what actually matters โ the agent's capabilities:
- PR creation and review: "Create a PR from my current branch with a summary of what changed" โ Cursor writes the description from the diff and pushes it
- Issue triage: "Look at the open issues labeled 'bug' and suggest which ones I should tackle first based on the code involved"
- Cross-repo context: "Check the API types in our shared-types repo and make sure this client code matches" โ Cursor fetches files from other repos via the API
- CI status checks: "Are there any failing checks on main? Show me the error logs" โ Cursor reads GitHub Actions results and helps debug
Every one of these requires a valid GitHub token. With TapAuth, that's one API call โ and you can go from zero to a working GitHub-connected agent in minutes, not days. Add Gmail next week? Same API, just change the provider name.
Scopes and permissions: Start narrow
GitHub OAuth scopes are coarse. Here's a practical guide:
| Scope | What it grants | When to use it |
|---|---|---|
repo | Full access to public and private repos | PRs, commits, branch management |
read:org | Read org membership and teams | When working with org repos |
read:user | Read user profile data | Identifying the authenticated user |
workflow | Update GitHub Actions workflows | CI/CD management (use carefully) |
If your workflow only needs read access, consider fine-grained tokens scoped to specific repos. TapAuth supports both token types and lets you configure scopes per connection.
Stop building OAuth plumbing for your agents
Every hour you spend implementing OAuth for an agent is an hour not spent on what the agent actually does. TapAuth is the token layer for AI agents โ one API call to connect any OAuth provider, with tokens that are scoped, time-limited, and user-approved.
Get started โ connect GitHub in under five minutes. No device flow polling. No token storage. Just tokens when your agent needs them.