---
title: "eve"
description: "Give an eve agent the GitHub tools, instructions on how to use them, and an approval policy from one file."
canonical_url: "https://github-tools.com/integrations/eve"
---
# eve

> Give an eve agent the GitHub tools, instructions on how to use them, and an approval policy from one file.

## Overview

`@github-tools/eve-extension` is an [eve extension](https://eve.dev/docs/extensions) that gives an eve agent the GitHub tools, a system instruction on how to use them, and an approval policy for every write tool, from one file under `agent/extensions/`.

Pick it when you build an eve agent. eve keeps approvals on the server, pauses the session until someone answers, asks users to sign in when a call needs their own GitHub access, and resumes after a crash, so you don't wire any of that yourself.

## Install

<code-group>

```bash [pnpm]
pnpm add @github-tools/eve-extension eve ai
```

```bash [npm]
npm install @github-tools/eve-extension eve ai
```

```bash [yarn]
yarn add @github-tools/eve-extension eve ai
```

```bash [bun]
bun add @github-tools/eve-extension eve ai
```

</code-group>

The whole agent is three files:

<code-group>

```ts [agent/agent.ts]
import { defineAgent } from 'eve'

export default defineAgent({
  model: 'anthropic/claude-opus-5.5',
})
```

```md [agent/instructions.md]
You are a GitHub assistant. Inspect pull requests and issues, and say what you changed.
```

```ts [agent/extensions/github.ts]
import githubExtension from '@github-tools/eve-extension'

export default githubExtension({
  preset: 'code-review',
})
```

</code-group>

Without a connector, the tools read `GITHUB_TOKEN`. Run `npx eve dev` and ask about a pull request. The agent reads with the `code-review` tools and stops before every write until someone approves it. [`examples/eve`](https://github.com/vercel-labs/github-tools/tree/main/examples/eve) is the same agent with a Vercel Connect connector and `'auto'` modes.

## Choose the tools

The mount file's name becomes the tool prefix: `agent/extensions/github.ts` exposes `github__listPullRequests`, `github__createIssue` and so on. Config keys such as `include` and `requireApproval` take the bare names.

`preset` sets which tools the model sees. `include` adds to the preset, so `{ preset: 'code-review', include: ['createIssue'] }` keeps the review tools and adds one write. Without a preset, `include` is the whole set. `exclude` removes tools from the result, after `preset` and `include`: `{ preset: 'maintainer', exclude: ['deleteGist'] }`.

### Let the model pick the tools

With `preset: 'auto'`, the decision model picks the tools the session's first request needs, and eve lists those and the [core read tools](https://github-tools.com/reference/glossary#core-read-tools). Every other tool is [deferred](https://eve.dev/docs/tools#keep-a-tool-out-of-the-tool-list): it's out of the tool list, but the model can still find it with `eve__search`.

The pick is stored with `ctx.memo`, so it stays fixed for the session and across processes. Set `evaluation.scope: 'turn'` to pick again for each request, or `evaluation.selection: 'presets'` to route to at most two presets instead.

## Approval

Every write tool asks by default. An approval pauses the session durably until someone answers. The prompt names the action and target, such as `Merge pull request on vercel-labs/github-tools#176?`, and after a write the channel shows `Created issue on vercel-labs/github-tools#212`. `owner`, `repo` and the number fall back to `context` when the call leaves them out.

With `'auto'`, the prompt also ends with the decision model's verdict, such as `Add labels on vercel-labs/github-tools#12? Hard to undo (risk 2 of 2), 100% likely you asked for it.` A call it lets through has `clearedBy: { policy: 'approval' }` on its `call.started` event, so you can tell automatic approvals from calls that needed none. eve doesn't keep the verdict of an automatic approval.

`requireApproval` takes one value for all write tools, or one per tool:

<table>
<thead>
  <tr>
    <th>
      Value
    </th>
    
    <th>
      What happens
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        true
      </code>
      
       or <code>
        'always'
      </code>
    </td>
    
    <td>
      Asks on every call
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        false
      </code>
      
       or <code>
        'never'
      </code>
    </td>
    
    <td>
      Never asks
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        'once'
      </code>
    </td>
    
    <td>
      Asks the first time in a session, then runs without asking
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        'auto'
      </code>
    </td>
    
    <td>
      The <a href="https://github-tools.com/approval#let-a-model-decide">
        decision model
      </a>
      
       approves routine writes the user asked for
    </td>
  </tr>
  
  <tr>
    <td>
      a function
    </td>
    
    <td>
      An eve approval policy over <code>
        toolInput
      </code>
      
       and the session. Return <code>
        true
      </code>
      
       to ask
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        { request, response }
      </code>
    </td>
    
    <td>
      An eve approval object, when you also decide who may answer
    </td>
  </tr>
</tbody>
</table>

This agent asks once per session before commenting, and asks before a merge only outside `vercel-labs`:

```ts [agent/extensions/github.ts]
export default githubExtension({
  preset: 'code-review',
  requireApproval: {
    addPullRequestComment: 'once',
    mergePullRequest: ({ toolInput }) => toolInput?.owner !== 'vercel-labs',
  },
})
```

### Add your own approval policy

An `approval` policy in `agent.ts` runs next to the extension's, and eve keeps the stricter result. It can add a gate but never remove one. Match the GitHub tools by prefix, as in `toolName.startsWith('github__')`.

### Limit where calls act

[`guardrails`](https://github-tools.com/approval#guardrails) limit the repositories and branches any call can reach, whoever approves it. A write outside them is denied with the reason, such as "`vercel/next.js` is outside the repositories these tools may act on", before anyone is asked:

```ts [agent/extensions/github.ts]
export default githubExtension({
  preset: 'pr-author',
  guardrails: { repositories: ['vercel-labs/*'], branches: ['agent/*'] },
})
```

They cover the extension's tools only. eve's git extension runs `gh` through `git__gh` without an approval prompt, so limit it with its own `github.authorize` check.

Set `dryRun: true` to try an agent on real repositories first: reads run, no write reaches GitHub, and the channel label reads "Would create issue on owner/repo". See [Shadow mode](https://github-tools.com/approval#shadow-mode).

## Use Vercel Connect

A connector replaces the long-lived token: each call mints a short-lived token scoped to the selected tools. With `preset: 'auto'`, each token covers only the tool being called. Install `@vercel/connect`:

<code-group>

```bash [pnpm]
pnpm add @vercel/connect
```

```bash [npm]
npm install @vercel/connect
```

```bash [yarn]
yarn add @vercel/connect
```

```bash [bun]
bun add @vercel/connect
```

</code-group>

Then pass the connector name instead of a token:

```ts [agent/extensions/github.ts]
export default githubExtension({
  connector: 'github/my-connector',
  preset: 'code-review',
})
```

[Auth](https://github-tools.com/auth#vercel-connect) covers creating the connector, and the permissions each preset needs. The token asks for the scopes of every selected tool, so the GitHub App needs all of them. `code-review`, `ci-ops` and `maintainer` include `createCommitStatus`, which needs Commit statuses (read and write). If your App doesn't have it, `exclude: ['createCommitStatus']` removes the tool and its scope together.

### Per-user tokens

By default every call uses the GitHub App's installation token, so anyone who can talk to the agent can do what the App can. `actAs` picks whose access a call uses:

<act-as-switch>



</act-as-switch>

<table>
<thead>
  <tr>
    <th>
      <code>
        actAs
      </code>
    </th>
    
    <th>
      A signed-in user drives the turn
    </th>
    
    <th>
      Someone comments on GitHub
    </th>
    
    <th>
      Nobody does: a schedule, <code>
        localDev()
      </code>
      
      , a bot
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        'app'
      </code>
      
       (default)
    </td>
    
    <td>
      The App
    </td>
    
    <td>
      The App
    </td>
    
    <td>
      The App
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        'user'
      </code>
    </td>
    
    <td>
      The user's own token
    </td>
    
    <td>
      Waits for a sign-in GitHub can't show
    </td>
    
    <td>
      Fails with <code>
        principal_required
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        'caller'
      </code>
    </td>
    
    <td>
      The user's own token
    </td>
    
    <td>
      The App, within the commenter's role
    </td>
    
    <td>
      The App
    </td>
  </tr>
</tbody>
</table>

This agent reads as the App, writes as the user when there is one, and always merges as the user:

```ts [agent/extensions/github.ts]
export default githubExtension({
  connector: 'github/my-connector',
  actAs: { read: 'app', write: 'caller' },
  overrides: { mergePullRequest: { actAs: 'user' } },
})
```

GitHub runs a user-token call only when both the user and the App have access, so a reader who asks for a merge gets GitHub's `403`. GitHub shows the action as the user, with the App's badge.

A user who hasn't authorized the App gets an eve sign-in prompt, and the call resumes after they authorize. Someone who can sign in never falls back to the App token. eve stores each user's token separately and never hands it to another user.

Someone who comments on GitHub can't sign in: every comment there is public, so [eve's GitHub channel](https://eve.dev/docs/channels/github) has nowhere to show the prompt. GitHub has already identified them, so with `'caller'` their calls run with the App's token, limited to their own role:

- Before each call, the extension reads the commenter's role on the repository the call targets. A read needs Read and a write needs Write, so someone who can write to one repository can't have the agent read another one they can't see.
- A refused call fails with [`CALLER_FORBIDDEN`](https://github-tools.com/errors#guardrails) before it reaches GitHub, and the message names the role they have and the one the call needs.
- `resolveReviewThread` names no repository, so the extension looks up the one the thread is in and needs Write there.
- A search runs only when the App's token reaches the thread's repository and no other, so it can't return results from a repository the commenter can't see. Each token is requested for that repository, and the extension asks GitHub what the token reaches before it searches. A `connect` config that pins an installation or several repositories widens the token, and the search is then refused.
- Every other tool without a repository is refused, since no role on a repository covers it: gists, notifications, `createRepository` and `forkRepository`.

The check is stricter than GitHub for the writes a reader can make themselves, such as a comment or a new issue. GitHub shows the action as the App.

### Who can approve

Only the person who asked for a call can approve it, so a colleague in a shared Slack thread can't approve your merge. Set `approvers: 'anyone'` to let any signed-in user approve and anyone deny. For `'user'` and `'caller'` tools, the approved call then runs with the approver's token, after they sign in. For `'app'` tools, the approval only gates the call. On GitHub, a `'caller'` call someone else approved runs as the App within the approver's role, with no sign-in.

For a custom rule, pass `{ request, response }` for that tool in `requireApproval`, with an eve [`response` policy](https://eve.dev/docs/tools/human-in-the-loop#authorizing-approval-responses). It replaces both defaults, and the sign-in checks still apply to the people it allows.

### Work across several installations

An App installed on several accounts needs no config. Each App token is minted for the installation that owns the call's `owner/repo`, after `context` defaults. Tools without a repository, such as `searchCode` and the gist tools, use the connector's default installation.

To pin one installation, set `installationId`, `authorizationDetails` or `repositories` in `connect`. For another rule, pass a resolver:

```ts [agent/extensions/github.ts]
export default githubExtension({
  connector: 'github/my-connector',
  connect: (ctx, call) => ({
    installationId: call.owner === 'hugorcd' ? 'inst_personal' : undefined,
  }),
})
```

## Run on eve's GitHub channel

When the agent runs on [eve's GitHub channel](https://eve.dev/docs/channels/github), the extension fills `context` from the thread: the repository, and the issue or pull request number. The model can leave them out of tool calls, approval prompts and labels name the thread, `guardrails` check the filled-in target, and the system prompt says which thread the agent is in. Fields you set in `context` take precedence, and turns from other channels are unchanged.

### Choose who the agent answers

By default, anyone who mentions the bot starts a turn, including on a public repository. `githubChannel` from `@github-tools/eve-extension/channels` is eve's `githubChannel` with one more option, `respondTo`:

```ts [agent/channels/github.ts]
import { connectGitHubCredentials } from '@vercel/connect/eve'
import { githubChannel } from '@github-tools/eve-extension/channels'

export default githubChannel({
  credentials: connectGitHubCredentials('github/my-agent'),
  respondTo: 'writers',
})
```

The values are named after GitHub's repository roles, and each one means that role and everyone above it:

<table>
<thead>
  <tr>
    <th>
      <code>
        respondTo
      </code>
    </th>
    
    <th>
      Who the agent answers
    </th>
    
    <th>
      GitHub role
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        'anyone'
      </code>
      
       (default)
    </td>
    
    <td>
      Anyone who can comment
    </td>
    
    <td>
      None needed
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        'triagers'
      </code>
    </td>
    
    <td>
      Triagers, writers, maintainers and admins
    </td>
    
    <td>
      Triage and above
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        'writers'
      </code>
    </td>
    
    <td>
      Writers, maintainers and admins
    </td>
    
    <td>
      Write and above
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        'maintainers'
      </code>
    </td>
    
    <td>
      Maintainers and admins
    </td>
    
    <td>
      Maintain and above
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        'admins'
      </code>
    </td>
    
    <td>
      Admins
    </td>
    
    <td>
      Admin
    </td>
  </tr>
</tbody>
</table>

The channel reads the commenter's role on the thread's repository before a comment starts or continues a turn. A comment from anyone below `respondTo` does nothing: no reaction, no session and no model call, so a stranger can't spend your model budget. A custom role counts as the base permission GitHub reports for it.

Every other option is eve's and passes through. With your own `onComment`, the role is checked for the comments it dispatches. `respondTo` applies to comments only: `onIssue`, `onPullRequest` and the CI hooks decide for themselves.

`respondTo` decides who can talk to the agent, and [`actAs: 'caller'`](#per-user-tokens) what a turn can do for them, on every repository it touches. An agent on the GitHub channel needs both. Approval, `'auto'` and `guardrails` still apply to whatever a turn does.

### Scheduled runs

A [markdown schedule](https://eve.dev/docs/schedules) has no channel, so a tool that needs approval fails the session instead of waiting. Let the writes a schedule makes run without asking only in scheduled sessions, and keep asking in interactive ones:

```ts [agent/extensions/github.ts]
import githubExtension from '@github-tools/eve-extension'

const unlessScheduled = ({ session }: { session: { schedule?: unknown } }) => session.schedule === undefined

export default githubExtension({
  preset: 'issue-triage',
  guardrails: { repositories: ['acme/*'] },
  requireApproval: {
    addLabels: unlessScheduled,
    addIssueComment: unlessScheduled,
  },
})
```

Pair it with `guardrails`, and try the schedule with `dryRun: true` first.

## Config

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      What it does
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        token
      </code>
    </td>
    
    <td>
      <code>
        string | () => Promise<string>
      </code>
    </td>
    
    <td>
      GitHub token or token provider. Defaults to <code>
        GITHUB_TOKEN
      </code>
      
       when <code>
        connector
      </code>
      
       isn't set
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        connector
      </code>
    </td>
    
    <td>
      <code>
        string | () => string | Promise<string>
      </code>
    </td>
    
    <td>
      Vercel Connect connector name, or a resolver that picks one per call. Wins over <code>
        token
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        actAs
      </code>
    </td>
    
    <td>
      <code>
        'app' | 'user' | 'caller' | { read, write }
      </code>
    </td>
    
    <td>
      Whose GitHub access a call uses with a connector. Default <code>
        'app'
      </code>
      
      . See <a href="#per-user-tokens">
        Per-user tokens
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        approvers
      </code>
    </td>
    
    <td>
      <code>
        'requester' | 'anyone'
      </code>
    </td>
    
    <td>
      Who may answer an approval. Default <code>
        'requester'
      </code>
      
      . See <a href="#who-can-approve">
        Who can approve
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        connect
      </code>
    </td>
    
    <td>
      <code>
        object | (ctx, call) => object
      </code>
    </td>
    
    <td>
      Connect token params for App tokens, such as <code>
        installationId
      </code>
      
      , <code>
        repositories
      </code>
      
       or <code>
        scopes
      </code>
      
      . Can't be combined with <code>
        actAs: 'user'
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        preset
      </code>
    </td>
    
    <td>
      preset name, array, or <code>
        'auto'
      </code>
    </td>
    
    <td>
      Which tools the model sees. See <a href="https://github-tools.com/presets">
        Presets
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        include
      </code>
    </td>
    
    <td>
      tool names
    </td>
    
    <td>
      Added to the preset, or the whole set without one
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        exclude
      </code>
    </td>
    
    <td>
      tool names
    </td>
    
    <td>
      Removed after <code>
        preset
      </code>
      
       and <code>
        include
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        context
      </code>
    </td>
    
    <td>
      <code>
        { owner?, repo?, pullNumber?, issueNumber?, ref? }
      </code>
    </td>
    
    <td>
      Defaults for tool inputs. See <a href="https://github-tools.com/context">
        Context
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        requireApproval
      </code>
    </td>
    
    <td>
      <code>
        boolean | 'auto' | { [writeTool]: value }
      </code>
    </td>
    
    <td>
      Approval for write tools. See <a href="#approval">
        Approval
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        guardrails
      </code>
    </td>
    
    <td>
      <code>
        { repositories?, branches?, secrets? }
      </code>
    </td>
    
    <td>
      Where any call may act, whoever approves it. A blocked write is denied with the reason instead of asking. See <a href="https://github-tools.com/approval#guardrails">
        Guardrails
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        dryRun
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      Shadow mode: reads run, no write reaches GitHub, and nobody is asked to approve one. See <a href="https://github-tools.com/approval#shadow-mode">
        Shadow mode
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        evaluation
      </code>
    </td>
    
    <td>
      object
    </td>
    
    <td>
      Tunes both <code>
        'auto'
      </code>
      
       modes: <code>
        model
      </code>
      
      , <code>
        maxRisk
      </code>
      
      , <code>
        minIntent
      </code>
      
      , <code>
        selection
      </code>
      
      , <code>
        scope
      </code>
      
      , <code>
        minToolProbability
      </code>
      
      , <code>
        maxTools
      </code>
      
      , <code>
        minPresetProbability
      </code>
      
      , <code>
        maxPresets
      </code>
      
      , <code>
        providerOptions
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        overrides
      </code>
    </td>
    
    <td>
      <code>
        { [tool]: { description?, approval?, actAs?, toModelOutput?, outputSchema? } }
      </code>
    </td>
    
    <td>
      Per-tool changes. <code>
        approval
      </code>
      
       takes the same values as <code>
        requireApproval
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        author
      </code>
      
      , <code>
        committer
      </code>
      
      , <code>
        coAuthors
      </code>
    </td>
    
    <td>
      <code>
        { name, email }
      </code>
    </td>
    
    <td>
      Commit identity for tools that commit. See <a href="https://github-tools.com/commits">
        Commits
      </a>
    </td>
  </tr>
</tbody>
</table>

eve checks the config against the extension's schema, so a misspelled tool name in `requireApproval`, or `actAs: 'user'` without a `connector`, fails instead of being ignored.

## FAQ

<accordion>
<accordion-item label="Which versions do I need?">

eve 1.0.0-rc.6 or later, Node.js 24, and `ai` 7, which eve itself needs.

</accordion-item>

<accordion-item label="Why does the extension accept any eve version?">

Your agent supplies eve, so the extension declares it as a peer with any version. eve checks the extension's build metadata instead.

</accordion-item>

<accordion-item label="What does the extension add to the system prompt?">

The extension adds the preset's tool rules, how paged lists and denied approvals work, and the working [context](https://github-tools.com/context). It leaves your agent's own instructions alone. The instruction depends only on the config, so the provider's prompt cache holds across model calls.

</accordion-item>

<accordion-item label="Do I need to sign approvals?">

No, the pending request stays on the server, so a client can't forge an answer.

</accordion-item>

<accordion-item label="Does 'auto' work in a subagent?">

No, `'auto'` always asks in a subagent, and for a `service` caller too, because their latest message wasn't written by a person.

</accordion-item>

<accordion-item label="Can I replace one of the extension's tools?">

Yes, a tool in `agent/tools/` with the same name, such as `github__createIssue.ts`, replaces the extension's.

</accordion-item>

<accordion-item label="What happens to the tools after a crash or a redeploy?">

The tools come back the same after a crash or a redeploy. The extension's tool file is a `defineDynamic` function that eve runs before every model call, so tools, schemas and approval policies are rebuilt from the config each time, including after a Workflow replay.

</accordion-item>

<accordion-item label="What does the model see when a call fails?">

The tool throws, so eve settles the call as `failed` and gives the model the error as the call's result. Hooks, instrumentation and `noFailedCalls()` in an eval see the failure. Errors the SDK recognizes carry a code, a cause and a fix from the [error catalog](https://github-tools.com/errors), and `call.settled` has the code in `error.code`.

</accordion-item>

<accordion-item label="Where is the rate limit?">

Tool results include `rateLimit` for your hooks and channels, and the extension strips it from what the model sees.

</accordion-item>

<accordion-item label="Can a write run twice after a crash?">

eve doesn't run a call again once its start was recorded. The model reads that the call may have taken effect and decides whether to repeat it. Repeating is safe for `closeIssue` on a closed issue, `createBranch` at the same SHA and `createOrUpdateFile` with unchanged content and `sha`, but not for `createIssue`, `addIssueComment` or `mergePullRequest`, and approval doesn't deduplicate them.

</accordion-item>

<accordion-item label="Can a connector reach gists?">

No, an installation token can't reach gists, so prefer presets without gist tools, such as `code-review`, over `maintainer` or `repo-explorer`.

</accordion-item>

<accordion-item label="Can I combine connect with actAs: 'user'?">

No, `connect` only configures App tokens.

</accordion-item>

<accordion-item label="What if a user's token is missing or rejected?">

When GitHub returns `401` for a user's token, eve asks that user to sign in again. A turn started by a schedule never opens a sign-in, so a call that needs one fails with `sign_in_unavailable`.

</accordion-item>

<accordion-item label="What if the App isn't installed on the target account?">

The tool returns `github_tools.CONNECT_INSTALLATION_REQUIRED`, naming that account.

</accordion-item>

<accordion-item label="Will the actAs default change?">

The `actAs` default may change to `'caller'` in a future major version.

</accordion-item>
</accordion>


## Sitemap

See the full [sitemap](https://github-tools.com/sitemap.md) for all pages.
