upseamdocs

Bring your own agent

Instead of a model key, a coding agent you already pay for can write the fixes: Claude Code, Codex, OpenCode, Hermes Agent or OpenClaw. Upseam still finds the change, matches it to your code, asks for approval, checks the result with its own gates and opens the pull request. Your agent runs in your GitHub Actions with your credentials, and Upseam never sees them.

Status. This mode is new. The reusable workflow below is built from the official documentation of each action but has not yet been run end to end on GitHub Actions, and it needs the first release of upseam/action. Try it on a test repository first.

Set it up

  1. Name the agent in .github/upseam.yml on the default branch:

    yaml

    agent: claude-code
    

    codex, opencode, hermes and openclaw work the same way. agent: claude-code is short for agent: { runner: claude-code }; both forms mean the same. The default upseam keeps the model from the settings page (see Connect a model). With any other value, this repository uses its own agent even when no model is connected.

  2. Add the short workflow for your agent as .github/workflows/upseam-agent.yml on the default branch: the same dozen lines for Claude Code, Codex, OpenCode, Hermes Agent and OpenClaw, only the runner, the model and the secret differ. Add your agent's credential as an Actions secret. Until a workflow there listens for repository_dispatch with the type upseam-agent, Upseam sends nothing and says what to add in the dashboard issue and in Slack.

  3. Pin the reusable workflow to the full commit SHA of an upseam/action release: git ls-remote https://github.com/upseam/action 'refs/tags/<tag>' 'refs/tags/<tag>^{}'. If a ^{} line is printed, use its SHA — the tag is annotated and that line is its commit — otherwise the plain line already is the commit.

What Upseam sends

After approval (the default ask mode) Upseam sends one repository_dispatch event with the type upseam-agent. It carries the group key, the commit to start from, the branch head Upseam expects, the id of this dispatch, the runner, the files the agent may edit, and a task for the agent: rules, those files, the matched places and the change data. The task has no file contents, no key and no token. The vendor's change text in it is marked as untrusted.

How an edit becomes a pull request

Your CI and other systems that build branches run upseam/* branches with their secrets before anyone reviews them, and workflow artifacts of a public repository can be downloaded by anyone signed in to GitHub. So nothing your agent writes leaves its job or reaches a branch before Upseam's checks pass:

  1. The agent job can only read the repository. It runs your agent.
  2. In the same job, a fresh checkout receives only the listed files, and upseam/action checks them: only edits of existing files, and the same patch gates as for a model's patch. A secret written into a file is a new literal, so the job stops. Only if the check passes are the files copied out of the workspace and uploaded.
  3. The push job runs no agent and holds no agent credential. It checks the files again and pushes one commit to upseam/<group>, marked with the id of the dispatch.
  4. Upseam checks the push once more. If anything fails, the finding moves to Needs you with the reason, no pull request opens, and Upseam moves the branch back to where it was. A push from an older dispatch is dropped the same way.

If nothing is pushed within an hour, the finding moves to Needs you with a hint to look at the upseam-agent workflow runs. The pull request says your agent wrote it and Upseam checked it.

The reusable workflow

Every agent uses the same reusable workflow, upseam/action/.github/workflows/agent.yml. Your file names it, pinned to a full commit SHA, and sets four things:

  • runner: claude-code, codex, opencode, hermes or openclaw, the same value as agent in .github/upseam.yml;
  • model: provider/model id for OpenCode, Hermes Agent and OpenClaw, left out for Claude Code and Codex;
  • model-key: the only secret the workflow receives, your agent's token or your model provider's API key;
  • permissions: contents: write: the most the workflow may use. It gives the agent only read access and write access only to the job that pushes.

The workflow first checks that the event is upseam-agent, that runner is one of the five and matches what Upseam sent, and that model names a provider the runner supports (anthropic, openai for OpenCode only, or openrouter); anything else stops the run before an agent starts. Then it runs your agent's job, the check in a fresh checkout and the upload, and finally the job that pushes, as described above. Every action in it is pinned to a commit, every image to a digest and every download to a hash. It never uses secrets: inherit.

You trust the workflow file at the SHA you pin instead of steps copied into your repository: read it at that SHA before pinning, and it changes only when you change the SHA. Don't pin a tag or a branch.

Claude Code

Claude Code runs through anthropics/claude-code-action. Create the secret CLAUDE_CODE_OAUTH_TOKEN with claude setup-token, or pass an Anthropic API key as model-key instead; a key that starts with sk-ant-oat goes to the action as a sign-in token, any other as an API key.

yaml

name: upseam-agent
on:
  repository_dispatch:
    types: [upseam-agent]
jobs:
  agent:
    uses: upseam/action/.github/workflows/agent.yml@<40-character SHA> # v0.x, after the first release
    permissions:
      contents: write
    with:
      runner: claude-code
    secrets:
      model-key: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
  • The agent gets only the Read, Edit, Glob and Grep tools. The settings block stops reads outside the repository and of /proc, where the agent's own environment lives, and edits of .git, workflow files and the runner's action and temporary directories. The paths assume GitHub-hosted Ubuntu runners.
  • github_token: ${{ github.token }} keeps the action on the read-only workflow token, so the Claude GitHub App is not needed.
  • allowed_bots: ${{ github.actor }} lets the Upseam App's bot start the action. Only accounts with write access can send the event at all.

Codex

openai/codex-action takes an OpenAI API key (secret OPENAI_API_KEY).

yaml

name: upseam-agent
on:
  repository_dispatch:
    types: [upseam-agent]
jobs:
  agent:
    uses: upseam/action/.github/workflows/agent.yml@<40-character SHA> # v0.x, after the first release
    permissions:
      contents: write
    with:
      runner: codex
    secrets:
      model-key: ${{ secrets.OPENAI_API_KEY }}

The workflow runs Codex in the :workspace permission profile, which limits where Codex writes, and the action removes sudo before Codex runs by default. Upseam has not checked which paths outside the workspace this profile lets Codex read.

OpenCode

OpenCode has no official GitHub Action for this, so the workflow runs the pinned CLI itself. It takes the key of the model provider you choose: Anthropic (ANTHROPIC_API_KEY), OpenAI (OPENAI_API_KEY) or OpenRouter (OPENROUTER_API_KEY), whose SDKs are built into OpenCode; set model to provider/model id. The workflow turns off OpenCode's built-in plugins, so their sign-in (OAuth or CLI) methods are unavailable, for example GitHub Copilot or ChatGPT sign-in for Codex.

yaml

name: upseam-agent
on:
  repository_dispatch:
    types: [upseam-agent]
jobs:
  agent:
    uses: upseam/action/.github/workflows/agent.yml@<40-character SHA> # v0.x, after the first release
    permissions:
      contents: write
    with:
      runner: opencode
      model: anthropic/<model id>
    secrets:
      model-key: ${{ secrets.ANTHROPIC_API_KEY }}
  • The job downloads OpenCode 1.18.32 for Linux x64 from npm and checks a pinned SHA-512 before unpacking it; no install script runs. Upseam checked that this binary is identical to the one in OpenCode's attested GitHub release. It also installs ripgrep 15.1.0, which OpenCode's search tools use, after checking its SHA-256, and marks OpenCode's plugin package as installed, so OpenCode itself downloads neither at startup.
  • The profile in OPENCODE_CONFIG_CONTENT denies every tool first, then allows only reading, searching and editing files in the repository. There is no shell, no web access, no subagents and no access outside the repository, including OpenCode's own tool-output directory. The agent cannot open .env files or .git, or edit workflow files or OpenCode's config. Its search tool does not apply these file rules, so it can show lines of a .env file that is committed to the repository; anyone who can read the repository can already see such a file.
  • Without --auto, a headless run rejects anything the profile does not allow. The workflow passes none of --auto, --yolo or --dangerously-skip-permissions.
  • Before the run, the step deletes any OpenCode config and symbolic links in the checkout, so the repository cannot add tools, plugins or MCP servers or point a file outside it. The task reaches OpenCode on stdin only.
  • Upseam checked these settings against the OpenCode docs and the source of this version but has not run this job end to end yet, and has not checked which model ids it accepts with the remote model list turned off.

Remaining risk (Claude Code, Codex, OpenCode)

  • The agent job holds your agent's credential while the agent reads vendor text that could try to steer it. The tool limits and read rules above are what keep the agent from reaching it.
  • In a public repository, the uploaded files and the run logs are public. Only files that passed the check are uploaded, so they hold nothing that would not be in the pull request.

Hermes Agent

Hermes Agent cannot be limited with its own settings the way Claude Code is: its file tools read any path its user can, including its own environment in /proc/self/environ, and its write guard is "not a hard boundary" by its own documentation. So Hermes runs inside a Docker container, and the container is the boundary. Create the Actions secret ANTHROPIC_API_KEY with an Anthropic API key.

yaml

name: upseam-agent
on:
  repository_dispatch:
    types: [upseam-agent]
jobs:
  agent:
    uses: upseam/action/.github/workflows/agent.yml@<40-character SHA> # v0.x, after the first release
    permissions:
      contents: write
    with:
      runner: hermes
      model: anthropic/claude-sonnet-4-6
    secrets:
      model-key: ${{ secrets.ANTHROPIC_API_KEY }}

For OpenRouter, set model to openrouter/ and an OpenRouter model id, such as openrouter/anthropic/claude-sonnet-4.6, and pass OPENROUTER_API_KEY as model-key.

How the sandbox works

  • Only the listed files. The job copies the files Upseam listed into a separate directory and mounts only that directory, at /work. The container sees no checkout, no .git, no .github, no runner home or temporary directory and no Docker socket.
  • A locked-down container. The root filesystem is read-only, /tmp is an in-memory scratch space, and Hermes runs as the unprivileged user 65534 with every Linux capability dropped, no-new-privileges and limits on memory (swap included) and processes.
  • Only the model key. The container gets the model key and a few Hermes settings, nothing else: no GITHUB_TOKEN, no GH_TOKEN, no Actions runtime token. What Hermes can read in /proc/self/environ is the key you already gave the model.
  • Only the model's host. The container sits on an internal Docker network with no route out and no address for the runner itself (Docker 28 or later, as on GitHub-hosted runners). Its one way out is a small proxy, shown in full in the reusable workflow, that only opens connections to PROVIDER_HOST on port 443. It logs the host of each allowed connection and a bare deny for each refused one, never what the client sent.
  • Only file tools. -t file gives Hermes read, write, patch and search. --safe-mode turns off plugins, MCP servers, hooks, user config and AGENTS.md injection; the environment turns off the tirith download and runtime package installs. Hermes sends no telemetry by default, and its update check has no git checkout to look at.
  • Checked like any other agent. After the container exits, only regular files at their listed paths, reached without symlinks, are copied back into a checkout the container never saw, and upseam/action checks them with the same patch gates before anything is uploaded or pushed.

Hermes is built from source at a fixed commit, f97608f (tag v2026.9.24, package version 0.21.5), checked with git rev-parse; its Python packages come from its own uv.lock, which fixes every file by hash, and only as prebuilt wheels. The base image is the uv image pinned by digest, the one Hermes' own Dockerfile uses as a build stage. Nous Research does not sign its release tags, so the commit hash is the only link to what they published.

Remaining risk (Hermes Agent)

  • Hermes can read the model key. The provider already has it; a listed file that contains it is a new literal the gates reject; Hermes' output and error output are not printed, only its exit code, since a reshaped copy would get past GitHub's log masking; and the proxy log holds no client data. What stays visible is whether the job passed, the exit code, how long it ran and how many connections the proxy allowed.
  • Hermes can fill the runner's disk by writing into /work; the job then fails, and nothing leaves it.
  • Vendor text can still steer the edits. The patch gates decide what reaches a branch.
  • Hermes sends the task and the files it reads to the model provider, as any agent does. It has no web or shell tool of its own.
  • The job needs a GitHub-hosted Ubuntu runner with Docker Engine 28 or later (for the isolated gateway). On Docker older than 26, an internal network may still send DNS queries out through the runner's resolver. Self-hosted runners are not supported: jobs sharing one Docker daemon would collide on the fixed container and network names, one job's cleanup could remove another's proxy, and a cancelled job could leave its agent container running.
  • The workflow is built from Hermes source and Docker documentation and has not yet been run on GitHub Actions.

OpenClaw

OpenClaw runs in the same sandbox as Hermes Agent. Its headless openclaw agent exec turns on the shell by default, so the job also denies every tool except the five file tools in its config; the container is still the boundary. Create the Actions secret ANTHROPIC_API_KEY.

yaml

name: upseam-agent
on:
  repository_dispatch:
    types: [upseam-agent]
jobs:
  agent:
    uses: upseam/action/.github/workflows/agent.yml@<40-character SHA> # v0.x, after the first release
    permissions:
      contents: write
    with:
      runner: openclaw
      model: anthropic/claude-sonnet-4-6
    secrets:
      model-key: ${{ secrets.ANTHROPIC_API_KEY }}

For OpenRouter, set model to an openrouter/... model id and pass OPENROUTER_API_KEY as model-key.

How the OpenClaw sandbox differs

  • The container, the mount, the environment and the proxy are the same as for Hermes Agent. The proxy runs from its Python image, OpenClaw from a Node image; both are pinned by digest.
  • OpenClaw gets only ls, read, write, edit and apply_patch, limited to the workspace. The shell, web, browser, sessions, memory, messaging, plugins and MCP tool groups are denied, the shell mode is deny, Code Mode is off, and skills, AGENTS.md-style context files and the update check are off.
  • OpenClaw is installed from npm at a fixed version whose package hash the build compares with the one in the job. Its dependencies are resolved as of the day of that release, and npm checks each against the registry's hash. The package's npm provenance names build commit a6e3d5d, not the commit the v2026.9.6 tag points to; the job does not check provenance.
  • The job runs OpenClaw with a pinned config file. The model key reaches it only through the environment: the run starts with an empty state directory and an empty home, so there are no stored credentials to find.

Remaining risk (OpenClaw)

  • The same as for Hermes Agent: where the model key can go, disk use in /work, vendor text steering the edits, the runner requirements (Docker 28, DNS on Docker older than 26, no self-hosted runners), and the workflow has not yet been run on GitHub Actions.
  • Upseam has not yet confirmed on a run that OpenClaw starts on a read-only root, that the final tool list is exactly the five file tools, and that the npm package matches its build commit.

Terms

  • Anthropic documents claude setup-token and CLAUDE_CODE_OAUTH_TOKEN for GitHub Actions. Its legal and compliance page says subscription sign-in is meant for ordinary use of Claude Code. Upseam never receives or routes your token, but whether a workflow that Upseam triggers counts as ordinary use is not stated. Use an API key if in doubt; Anthropic also recommends one for a secret shared across an organization.
  • OpenAI recommends API keys for Codex in CI, and the Codex workflow uses one.
  • OpenCode is open source (MIT) and uses the model provider's key you give it, so that provider's terms apply. The OpenCode job uses an API key.
  • Hermes Agent is MIT-licensed and sends requests with the key you give it, so the model provider's terms apply. Use an API key: Anthropic's page above reserves subscription sign-in for Claude Code and other native Anthropic applications.
  • OpenClaw is MIT-licensed and, like Hermes Agent, calls the provider with the key you give it; the workflow expects an API key.