The real problem
Why this is harder than it looks
The GitHub API documentation is excellent. The OAuth flow is standard. You can have a working proof-of-concept in an afternoon. The complexity arrives when you try to build this for real users, in a product, at scale — and it’s messier than most connectors.
The first thing most teams discover is that GitHub has two meaningfully different auth systems with confusingly similar names: OAuth Apps and GitHub Apps. For agents acting on behalf of individual users, you’ll use OAuth 2.0 user access tokens. But OAuth Apps carry a hard platform limit of 10 active tokens per user per scope combination. In a multi-tenant product — or even a developer running multiple clients — this means old tokens silently get revoked when new ones are issued. Your agent will start returning 401s for some users with no explanation. The fix (switching from an OAuth App to a GitHub App) is non-trivial to discover, and the error message gives you nothing to go on.
Then there’s the scope problem. GitHub’s OAuth scopes are coarse-grained by default. The repo scope — which you need for any private repository work — grants full read and write access to every repository the user can access. Enterprise security teams notice this. Users notice it. The consent screen asks for a lot, and that creates friction at the authorization step for corporate GitHub accounts.
Beyond that: per-user token isolation in a multi-tenant system, proactive refresh logic before expiry, and revocation detection when users remove your OAuth authorization from their GitHub settings. Each is manageable individually. Together, they’re the kind of work that keeps a sprint busy without advancing your agent’s actual capabilities. Scalekit handles all of it so your code only has to name the tool and pass parameters.
Capabilities
What your agent can do with GitHub
Once connected, your agent has 9 pre-built tools covering the core GitHub developer workflow:
- Read and write repository files: fetch file contents, create new files, or update existing files with a commit message — all in one call
- Manage issues end-to-end: create issues with labels, assignees, and milestones; list and filter by state, author, label, or date
- Open and list pull requests: create PRs from a feature branch to a base, list with state and branch filters, support for draft PRs
- Inspect repositories: get detailed repo metadata and settings; list public repos for any user or the authenticated user’s full repo set
Setup context
What we’re building
This guide connects a developer assistant agent to GitHub — helping engineers manage issues, review open PRs, and create or update files in their repos without leaving your product.
🤖
Example agent
Developer assistant triaging issues, surfacing open PRs, and committing file changes on behalf of each engineer
🔐
Auth model
B2B SaaS — each engineer connects their own GitHub account. identifier = your user ID
Setup
1 Setup: One SDK, One credential
Install the Scalekit SDK. The only credential your application manages is the Scalekit API key — no GitHub secrets, no user tokens, nothing belonging to your users.
pip install scalekit-sdk-python
import scalekit.client
import os
from dotenv import load_dotenv
load_dotenv()
scalekit = scalekit.client.ScalekitClient(
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
env_url=os.getenv("SCALEKIT_ENV_URL"),
)
actions = scalekit.actions
Connected Accounts
2 Per-User Auth: Creating connected accounts
Each engineer gets their own Connected Account, giving them a dedicated auth context. The identifier is any unique string from your system — a UUID, email, whatever you use internally.
response = actions.get_or_create_connected_account(
connection_name="github",
identifier="user_gh_456" # your internal user ID
)
connected_account = response.connected_account
print(f"Status: {connected_account.status}")
# Status: PENDING — user hasn't authorized yet
This call is idempotent — safe to call on every session start. Returns the existing account if one already exists.
Authorization Flow
3 The authorization flow
The engineer authorizes your agent once. Scalekit generates the OAuth URL with correct scopes, PKCE challenge, and redirect handling pre-configured. After approval, you never see the token.
if connected_account.status != "ACTIVE":
link = actions.get_authorization_link(
connection_name="github",
identifier="user_gh_456"
)
# Redirect user → GitHub OAuth consent screen
# Scalekit captures the token on callback
return redirect(link.link)
Token management is automatic
After the user approves, Scalekit stores encrypted tokens and the connected account moves to ACTIVE. Access
tokens refresh before expiry. If an engineer revokes access from their GitHub settings, the account moves to REVOKED
— no silent failures. Check account.status before critical write operations.
Bring Your Own Credentials for production
For production deployments, register your own GitHub OAuth App in GitHub Developer Settings and supply your
Client ID and Secret in the Scalekit dashboard. Users will see your app name on the consent screen.
Token management stays fully handled.
Already have credentials?
Calling GitHub
4 Calling GitHub: What your agent writes
With the connected account active, your agent calls GitHub actions using execute_tool. Name the tool, pass parameters. Scalekit handles token retrieval, request construction, and response parsing.
List a user’s repositories
Fetch all repositories accessible to the authenticated user. Use type and sort to narrow results — useful for letting an agent discover what the user has access to before taking action.
result = actions.execute_tool(
identifier="user_gh_456",
tool_name="github_user_repos_list",
tool_input={
"type": "owner",
"sort": "updated",
"per_page": 20
}
)
# Returns array of repo objects with name, description, visibility, updated_at
List open issues with filters
Pull all open issues from a repo, filtered by label, assignee, or update date. The since parameter is ISO 8601 — useful for “what’s changed since the last standup” queries.
result = actions.execute_tool(
identifier="user_gh_456",
tool_name="github_issues_list",
tool_input={
"owner": "acme-corp",
"repo": "backend-api",
"state": "open",
"labels": "bug,p1",
"sort": "updated",
"per_page": 50
}
)
Create an issue
Open a new issue with title, body, labels, and assignees. Setting assignees or milestones requires the connected user to have push access to the target repo.
result = actions.execute_tool(
identifier="user_gh_456",
tool_name="github_issue_create",
tool_input={
"owner": "acme-corp",
"repo": "backend-api",
"title": "Auth token refresh silently fails on 429 response",
"body": "Reproducible in staging. Steps to reproduce: ...",
"labels": ["bug", "p1"],
"assignees": ["jdoe"]
}
)
# Returns: { "number": 142, "html_url": "https://github.com/...", ... }
Read and update a file in a repo
Get a file’s current contents (returned Base64 encoded), then write an updated version back. The sha from the get response is required when updating — GitHub uses it to detect conflicting edits.
import base64
# 1. Read the current file
current = actions.execute_tool(
identifier="user_gh_456",
tool_name="github_file_contents_get",
tool_input={
"owner": "acme-corp",
"repo": "backend-api",
"path": "docs/CHANGELOG.md",
"ref": "main"
}
)
# 2. Decode, modify, re-encode
original = base64.b64decode(current["content"]).decode()
updated = "## v2.4.1\n- Fixed auth token refresh\n\n" + original
encoded = base64.b64encode(updated.encode()).decode()
# 3. Write back — sha is required for updates
result = actions.execute_tool(
identifier="user_gh_456",
tool_name="github_file_create_update",
tool_input={
"owner": "acme-corp",
"repo": "backend-api",
"path": "docs/CHANGELOG.md",
"message": "docs: add v2.4.1 changelog entry",
"content": encoded,
"sha": current["sha"], # required for updates
"branch": "main"
}
)
Framework wiring
5 Wiring into your agent framework
Scalekit integrates directly with LangChain. The agent decides what to call; Scalekit handles auth on every invocation. No token plumbing in your agent logic.
from langchain_anthropic import ChatAnthropic
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from scalekit.langchain import get_tools
gh_tools = get_tools(
connection_name="github",
identifier="user_gh_456"
)
prompt = ChatPromptTemplate.from_messages([
("system", "You are a developer assistant. Use the available tools to help manage GitHub repositories, issues, and pull requests."),
MessagesPlaceholder("chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])
agent = create_tool_calling_agent(ChatAnthropic(model="claude-sonnet-4-6"), gh_tools, prompt)
result = AgentExecutor(agent=agent, tools=gh_tools).invoke({
"input": "Show me all open P1 bugs in the backend-api repo and create a summary issue linking them"
})
Other frameworks supported
Tool reference
All 9 GitHub tools
Grouped by capability. Your agent calls tools by name — no API wrappers to write.
Get detailed metadata, settings, and statistics for a specific repository by owner and repo name
List all repositories accessible to the authenticated user, with sort, type, and pagination filters
List public repositories for any specified GitHub username — does not require authentication
Get the contents of a file or directory. Returns Base64 encoded content and SHA for files. Supports branch, tag, and commit ref
github_file_create_update
Create a new file or update an existing one with a commit message. Requires SHA when updating. Supports custom author and committer metadata
Create a new issue with title, body, labels, assignees, and milestone. Assignees require push access
List issues filtered by state, label, assignee, creator, milestone, and date. Pagination supported
github_pull_request_create
Create a pull request from a head branch into a base branch. Supports draft PRs and maintainer modification flag
github_pull_requests_list
List pull requests filtered by state, head branch, or base branch. Pagination and sort supported
Connector notes
GitHub-specific behavior
File updates require the current file’s SHA
When using github_file_create_update to modify an existing file, you must supply the blob sha from a prior
github_file_contents_get call. GitHub uses this to detect and prevent conflicting edits. Omitting it on an
update returns a 422 — not a 404 or auth error. Always fetch before writing.
File contents are Base64 encoded
The GitHub Contents API returns file content as Base64. When reading, decode before use. When writing,
encode the full file content before passing it as content. This applies to both text and binary files.
See the code example in Step 4 for the standard pattern.
Issues API returns pull requests too
GitHub’s REST API returns pull requests as a type of issue. When using github_issues_list, results may
include PRs alongside regular issues. If your agent needs to distinguish between them, check for the
presence of the pull_request key in each result object.
Infrastructure decision
Why not build this yourself
The GitHub OAuth flow is documented. Token storage isn’t technically hard. But here’s what you’re actually signing up for:
PROBLEM 01
GitHub OAuth Apps have a hard limit of 10 active tokens per user per scope combination — exceeding it silently revokes your oldest tokens with no warning
PROBLEM 02
Access token expiry requires proactive refresh logic running before the token expires, not in response to a failed API call mid-operation
PROBLEM 03
Revocation detection when users remove your OAuth authorization from GitHub Settings — and graceful handling so agents stop making calls immediately
PROBLEM 04
Per-user token isolation across a multi-tenant system — one user’s GitHub credentials must never be accessible to another, even under the same organization
That’s one connector. Your agent product will eventually need Salesforce, Slack, Jira, Notion, and whatever else your customers ask for. Each has its own OAuth quirks and failure modes.
Scalekit maintains every connector. You maintain none of them.