Build a GitHub agent with the eve extension
eve is Vercel's filesystem-first agent framework: an agent is a folder with instructions, a model config, and tools. @github-tools/eve-extension packages all 84 GitHub tools as a mountable eve extension: a single pnpm add and a one-line mount under agent/extensions/, no CLI setup, and no direct SDK import in agent/tools/.
createGithubTools and per-tool factories from @github-tools/sdk/eve) are deprecated in its favor. They keep working for existing agent/tools/ setups, but new agents should mount the extension instead. Shared runtime helpers used by this extension live on @github-tools/sdk/eve-runtime (not deprecated).Add GitHub tools to an eve agent via the extension
The whole agent
Three files. That's the entire thing:
You are a GitHub assistant. Use the GitHub tools to inspect repos, PRs, and issues.
Ask before merging or closing anything destructive.
import { defineAgent } from 'eve'
export default defineAgent({
model: 'anthropic/claude-opus-5.5',
})
import githubExtension from '@github-tools/eve-extension'
export default githubExtension({
preset: 'maintainer',
})
Run it:
npx eve dev
You now have a GitHub agent that can read repos, review PRs, triage issues, and manage CI, with every write operation gated behind durable approval by default. The full example lives in examples/eve/ (or pnpm dev:eve from the monorepo root).
For complete production implementations, see the Software Factory and Incident Response Agent. Both use the eve extension with Vercel Connect, while scoping GitHub access for different operational needs.
Install
pnpm add @github-tools/eve-extension
npm install @github-tools/eve-extension
yarn add @github-tools/eve-extension
bun add @github-tools/eve-extension
eve is a required peer dependency (which itself requires ai v7). The extension declares it as *: your agent provides the runtime copy of eve, and eve validates the extension's generated capability metadata at build time instead of an npm range. This release is built against eve 1.0.0-rc.1 and mounts on eve 1.0.0-rc.1 or later:
pnpm add eve
npm install eve
yarn add eve
bun add eve
You still need GITHUB_TOKEN (or a Vercel Connect connector, below). See Installation.
Mount it
Drop a file under agent/extensions/. The filename becomes the tool namespace:
import githubExtension from '@github-tools/eve-extension'
export default githubExtension({
connector: 'github/my-connector', // or token: process.env.GITHUB_TOKEN
preset: 'code-review',
requireApproval: {
addPullRequestComment: ({ toolInput }) => toolInput?.owner !== 'vercel-labs',
},
})
Tools are exposed to the model as <namespace>__<toolName>, where <namespace> comes from the mount file's name: agent/extensions/github.ts yields github__listPullRequests, github__createIssue, and so on.
code-review is used above (rather than maintainer) because it pairs cleanly with a Connect connector. maintainer and repo-explorer include gist tools, and GitHub only grants gist access to user access tokens, never the installation tokens Connect mints, so gist calls 403 over Connect (see Tokens & Auth). The requireApproval predicate above is a real gate, not a no-op: write tools already require approval by always() by default, so { mergePullRequest: true } would change nothing. A predicate is what actually narrows or loosens the default.
Pick exact tools
preset scopes to one of the ten predefined presets. To hand-pick tools instead, standalone or layered on top of a preset, use include and exclude:
import githubExtension from '@github-tools/eve-extension'
export default githubExtension({
include: ['getRepository', 'listPullRequests', 'mergePullRequest'],
})
include adds to preset. The effective set is the union of both, so you can pull in a tool a preset is missing without switching to a bigger preset. When connector is set and connect.scopes is omitted, Connect scopes follow that same resolved tool set — a standalone include does not mint the full admin union:
export default githubExtension({
preset: 'code-review', // read-only PR review tools
include: ['createIssue'], // + one write tool code-review doesn't include
})
exclude removes tool names from whatever preset + include resolved to. Useful for dropping a couple of tools you don't want exposed from a larger preset:
export default githubExtension({
preset: 'maintainer', // all 84 tools
exclude: ['createRepository', 'deleteGist'], // minus these two
})
Automatic presets and approval
For an agent that takes open-ended requests, let the extension pick the tools and decide which writes need a human. Both modes need ai 7.0.105 or later.
import githubExtension from '@github-tools/eve-extension'
export default githubExtension({
preset: 'auto',
requireApproval: 'auto',
})
preset: 'auto'asks the evaluation model which presets the latest user message needs and registers only their tools for that turn. It combines at most two presets and never exposes the full catalog. Tools the agent already called in the session stay registered, so a parked approval resumes even when routing changes.requireApproval: 'auto'lets low-risk write tools (AUTO_APPROVAL_TOOLS: labels, assignees, reactions, comments, review-thread replies, reviewer requests, notification reads, workflow re-runs) run without a prompt when the model rates the call low-risk and the user's request asked for it. Other write tools keepalways(). Mix it per tool:requireApproval: { addLabels: 'auto', mergePullRequest: true }.evaluationtunes both:minPresetProbabilityandmaxPresetsfor routing,maxRiskandminIntentfor approval, andmodelto replace the default evaluation model.
Config schema
| Field | Type | Notes |
|---|---|---|
token | string | (() => Promise<string>) | PAT string, or an async provider for rotating tokens (e.g. a GitHub App installation token) — the same GithubTokenInput the SDK accepts; falls back to GITHUB_TOKEN when omitted and connector is not set |
connector | string | (() => string | Promise<string>) | Vercel Connect connector name, or a resolver to pick one dynamically (e.g. per environment/tenant); takes priority over token |
connect | record | ((ctx, call) => record) | Passed through to getToken when connector is set; connect.subject defaults to { type: 'app' } and also accepts a per-caller resolver, see Per-user tokens. App tokens target the installation owning each call's repository by default, see Multiple GitHub App installations; a resolver picks other params per tool call |
preset | preset name, array, or 'auto' | code-review, issue-triage, ci-ops, repo-explorer, security-audit, release-manager, discussion-moderator, notification-inbox, pr-author, maintainer, see Presets. 'auto' routes each user message, see Automatic presets and approval |
include | string[]? | Tool names to add on top of preset (union), or the full set standalone, see Pick exact tools |
exclude | string[]? | Tool names to remove from the resolved preset + include set |
context | { owner?, repo?, pullNumber?, issueNumber?, ref? }? | Default owner/repo/number/ref for tool inputs — matching fields become optional and fill from context when omitted, see Working context |
requireApproval | boolean | 'auto' | record | Global or per-tool; per-tool values may be 'once', 'always', 'never', 'auto', or predicate functions |
evaluation | { model?, maxRisk?, minIntent?, minPresetProbability?, maxPresets? }? | Tunes the 'auto' modes, see Automatic presets and approval |
overrides | record | Per-tool description / approval / toModelOutput / outputSchema |
author / committer / coAuthors | commit identity | Attribution for commit-creating tools, see Commit Attribution |
The provider runs on every tool call, so short-lived tokens stay fresh:
import githubExtension from '@github-tools/eve-extension'
export default githubExtension({
token: () => mintInstallationToken(),
preset: 'issue-triage',
})
Durable multi-turn sessions
The extension's tool file is a defineDynamic function that eve runs before every model call. It rebuilds each tool's execute, toModelOutput, approval, and schemas from the extension config with @github-tools/sdk/eve-runtime, so the same tools and approval policies come back in every process, including after Workflow replay and redeploys. With preset: 'auto', the routed presets are kept for the turn with ctx.memo, so every model call in a turn sees the same tools. Prefer this mount over the deprecated createGithubTools / connectGithubTools paths.
Object-shaped execute results include rateLimit (remaining, limit, reset, resource). toModelOutput strips it so the model never sees the remaining count; toolResultFrom and channels still do. See Rate-limit metadata.
If execute fails (token mint, GitHub 403/429, …), the tool returns { error } instead of throwing so the model always receives a tool_result. A thrown error in eve's tool-loop can leave a tool_use unpaired and kill the turn. For failures the SDK can classify, error is a structured { code, message, why, fix, link } object from the error catalog — e.g. a 404 explains that GitHub also masks no-access private repos as 404, so the model does not hallucinate a cause. Unclassified failures keep the plain message string.
Durable approval, done right
Approval pauses the session durably until a human responds, and policies are expressive:
| Value | Maps to | Behavior |
|---|---|---|
true / 'always' | always() | Require approval on every call |
false / 'never' | omit approval | Skip approval (eve default) |
'once' | once() | Approve once per session, then auto-allow |
| predicate | custom Approval | Input-dependent gate (toolInput, session context) |
'auto' | evaluation model | Low-risk write tools run when the call matches the user's request; otherwise approval is requested |
Default (no requireApproval): all write tools → always(). Unlisted write tools keep the always() fail-safe default. Read tools never require approval. Details: Control write safety.
Approval requests name the action and its target instead of eve's default Approve <tool>?, for example Merge pull request on vercel-labs/github-tools#176?. owner, repo, and the PR, issue, or discussion number fall back to context when the call omits them. Slack also shows the full tool input in a collapsed block below the card.
Vercel Connect
Skip GITHUB_TOKEN entirely and mint the token from a Connect connector. Pass connector directly in the mount config, no separate connectGithubTools import needed:
import githubExtension from '@github-tools/eve-extension'
export default githubExtension({
connector: 'github/my-connector',
preset: 'code-review',
})
Unlike the deprecated direct import, no build.externalDependencies workaround is needed in agent/agent.ts. The extension is pre-built via eve extension build and loaded through eve's extension mechanism rather than inlined from a workspace-linked source import.
@vercel/connect is an optional peer dependency of the extension, install it only when using connector. connector also accepts a () => string | Promise<string> resolver for picking a connector per environment or tenant, see dynamic connector selection. See Vercel Connect for the connector setup checklist and multi-tenant scoping.
Per-user tokens
By default Connect mints the project's GitHub App installation token (subject: { type: 'app' }) — one identity shared by every caller. That is right for single-tenant agents, but in multi-user apps where each user connects their own GitHub account, it silently gives every signed-in user the project-level access. Set connect.subject to a per-caller resolver to mint each caller's own connection token instead. The resolver receives the eve tool execution context on every tool call:
import githubExtension from '@github-tools/eve-extension'
export default githubExtension({
connector: 'github/my-connector',
preset: 'issue-triage',
connect: {
subject: (ctx) => {
const caller = ctx.session.auth.current
if (!caller) throw new Error('GitHub tools require an authenticated caller')
return { type: 'user', id: caller.principalId }
},
},
})
ctx.session.auth.current is the authenticated caller of the active turn — eve restores it durably across workflow replay, so the resolver stays correct in multi-turn sessions. A caller without an active GitHub connection gets a UserAuthorizationRequiredError from Connect instead of silently falling back to the app installation. A static subject value (e.g. { type: 'user', id } fixed at mount time) also works when the agent serves a single known user.
Multiple GitHub App installations
Nothing to configure. Each app token is minted for the GitHub App installation that owns the call's target repository, so one App installed on several orgs or users just works:
import githubExtension from '@github-tools/eve-extension'
export default githubExtension({
connector: 'github/my-connector',
preset: 'pr-author',
context: { owner: 'evloghq', repo: 'evlog' },
})
The target is the tool's owner / repo input after context defaults are applied, so a call that omits them uses the configured home repository. Tools without a repository target (searchCode, gist and notification tools) get the connector's default installation. Static connect params (validityBufferMs, scopes, ...) merge in. Connect caches tokens per connector and params, so repeated calls on the same repository reuse one token. When the App is not installed on the target account, the tool returns CONNECT_INSTALLATION_REQUIRED naming that account instead of a GitHub 403.
To pin one installation instead, set installationId, authorizationDetails or repositories in connect; an explicit choice is never overridden. User-subject tokens already span installations and are left untouched.
For other per-call rules, pass your own resolver. It receives the eve tool execution context and the call ({ toolName, input, owner?, repo? }), and may return a subject resolver too:
export default githubExtension({
connector: 'github/my-connector',
connect: (ctx, call) => ({
installationId: call.owner === 'hugorcd' ? 'inst_personal' : undefined,
}),
})
Idempotency
eve replays completed steps but re-runs steps interrupted mid-execution:
| Tool | Idempotency |
|---|---|
createOrUpdateFile | Natural when content + sha unchanged |
closeIssue | Natural when already closed |
createBranch | Natural when branch exists at same SHA |
addIssueComment, createIssue, mergePullRequest, … | Not idempotent |
Gate non-idempotent writes behind always() or once() where replay safety matters.
Migrating from the direct import
If you have an existing agent using @github-tools/sdk/eve directly in agent/tools/, move to the extension in three steps:
pnpm add @github-tools/eve-extensionand remove the now-unused directeve/ai/zodinstall if nothing else in the agent needs them.- Delete
agent/tools/github.tsand createagent/extensions/github.tsexportinggithubExtension({ ...same options... })instead ofcreateGithubTools({ ...same options... }). Options (preset,include,exclude,context,requireApproval,overrides,author/committer/coAuthors,token) are unchanged. - If you used
connectGithubToolsfrom@github-tools/sdk/connect/eve, drop it. Passconnectordirectly togithubExtensioninstead. - If you cherry-picked single-tool factories (e.g.
listPullRequests()exported alone from its own file), replace eachagent/tools/*.tsfile with aninclude: [...]list in the singleagent/extensions/github.tsmount, see Pick exact tools.
One behavior change to be aware of: tool names gain the <namespace>__ prefix described above, so update any code that references tool names by their bare string (requireApproval and include keys stay bare, only the exposed model-facing tool name gets the prefix).
eve extension vs direct import vs AI SDK vs Workflow SDK
| eve extension | eve (direct import) | AI SDK | Workflow SDK | |
|---|---|---|---|---|
| Import | @github-tools/eve-extension | @github-tools/sdk/eve (createGithubTools) | @github-tools/sdk | @github-tools/sdk/workflow |
| Mount point | agent/extensions/ | agent/tools/ | anywhere | workflow function |
| Status | Recommended | Deprecated | Active | Active |
| Tool registration | githubExtension() mount | defineDynamic in agent/tools/ | createGithubTools() object | createGithubTools() in workflow |
| Tool naming | <namespace>__<toolName> | bare tool name | bare tool name | bare tool name |
| Approval | always / once / predicates | always / once / predicates | boolean needsApproval | boolean needsApproval (durable pause) |
| Durability | eve session (filesystem-first) | eve session (filesystem-first) | in-process | "use workflow" steps |
External references
- How to build a GitHub agent with eve and GitHub tools
- eve documentation
- eve extensions
- Dynamic capabilities (bundled with the
evepackage) - Human-in-the-loop