Dependabotfor your APIs.

An API you use changes. A pull request with the fix shows up.

  • Stripe
  • OpenAI
  • Anthropic
  • Gemini
  • Mistral
  • Shopify
  • HubSpot
  • Twilio
  • GitHub

Works with

Agents write the fix; editors get a PR when a tracked MCP server renames a tool. Trademarks belong to their owners; no endorsement implied.

01 / How it works

From their changelog
to your pull request.

OpenAI retires gpt-4-0613 on 2026-10-23 for gpt-5.6-sol.

Preview — product recording coming soon
  1. 01Detect A vendor API changes.
  2. 02Match The lines that use it.
  3. 03Patch Your model writes the fix.
  4. 04PR Gated, then reviewed by you.
  5. 05Slack A report, or a question first.
  1. 01 · Vendor change

    The vendor ships a change.

    Upseam reads vendor changelogs every hour, and your own contracts on every push.

    developers.openai.com/api/docs/deprecations

    2026-04-22

    Legacy GPT model snapshots

    Access to these models will be shut down on the dates below.

    Model Shutdown Replacement
    gpt-3.5-turbo-0125 2026-10-23 gpt-5.6-terra
    gpt-4-0613 2026-10-23 gpt-5.6-sol
    Upseam · new breaking change
  2. 02 · Affected code

    It finds the lines it touches.

    Down to the file and line. No match, no noise.

    acme/ai-app · src/chat.ts
    import OpenAI from "openai";const client = new OpenAI();export async function answer(question: string) {  const reply = await client.chat.completions.create({    model: "gpt-4-0613",    messages: [{ role: "user", content: question }],  });

    2 matches in acme/ai-app

    src/chat.ts:7 gpt-4-0613 fixable

    src/chat.ts:22 Assistants API needs you

  3. 03 · Pull request

    A pull request, in your style.

    Your model writes the fix in your conventions. What it can’t change safely is listed. Your CI runs on it.

    github.com/acme/ai-app/pull/57

    Replace retired OpenAI model gpt-4-0613 with gpt-5.6-sol #57

    Open upseam wants to merge 1 commit into main

    Changes

    src/chat.ts line 7

    - model: "gpt-4-0613",

    + model: "gpt-5.6-sol",

    Not changed: needs you

    src/chat.ts line 22 calls the Assistants API, retired on 2026-08-26.

    Verification

    Upseam checks passed: 1 file, 1 line. Your CI: pending.

    Written by the model you connected, with your key
  4. 04 · Slack

    Or ask first, in Slack.

    In ask mode nothing is pushed until someone presses Open PR. Merge stays in GitHub.

    # api-changes

    Upseam App

    OpenAI retires gpt-4-0613 on 2026-10-23

    acme/ai-app uses it in src/chat.ts:7. The vendor names gpt-5.6-sol as the replacement. Upseam can write the fix with your model and open a PR.

    Also found: Assistants API (retired 2026-08-26) in src/chat.ts:22, needs you.

    PR #57 opened · approved by @maria · CI pending

    Illustrative message

Illustrative run on published data. Source: OpenAI deprecations

02 / Features

Built like Dependabot.
Aimed at APIs.

Your GitHub, your Slack, your model or your agent.

  1. 01 / Match

    Finds the call sites the change touches

    JavaScript/TypeScript, Python and Go.

  2. 02 / Detect

    From vendor changelog to pull request

    OpenAI retires gpt-4-0613; the pull request moves to gpt-5.6-sol.

  3. 03 / Review

    Approve in Slack, merge in GitHub

    Upseam opens the pull request. It never merges.

  4. 04 / Patch

    Fixes that follow your conventions

    Small diffs, next to the matched lines.

  5. 05 / Watch

    Watches the APIs you already call

    Stripe, OpenAI, Anthropic, Mistral, Shopify, GitHub.

  6. 06 / Model

    Your model, your keys

    Billed by your provider.

Trademarks belong to their owners; no endorsement implied.

03 / Coverage

Breaking changes.
New capabilities. Your own APIs.

Mechanical fixes arrive as pull requests. Everything else is flagged.

APIs watched: 11 — see all

APIs watched

11

9 API providers and 2 MCP servers, plus your own OpenAPI and GraphQL contracts.

  • Stripe
  • Shopify
  • OpenAI
  • Anthropic
  • Gemini
  • Mistral
  • HubSpot
  • GitHub
  • Twilio
Learn more

APIs watched

Close

11 in the catalog: 9 API providers and 2 MCP servers.

  • Stripe API

    JavaScript / TypeScript · Python · Go

    527 changes · newest dated

    Changelog for Stripe
  • Shopify API

    JavaScript / TypeScript · Python

    316 changes · Upcoming:

    Changelog for Shopify
  • OpenAI API

    JavaScript / TypeScript · Python · Go

    141 changes · newest dated

    Changelog for OpenAI
  • Claude API

    JavaScript / TypeScript · Python · Go

    19 changes · newest dated

    Changelog for Claude
  • Gemini API

    JavaScript / TypeScript · Python · Go

    44 changes · newest dated

    Changelog for Gemini
  • Mistral API

    JavaScript / TypeScript · Python

    40 changes · newest dated

    Changelog for Mistral
  • HubSpot API

    JavaScript / TypeScript · Python

    235 changes · newest dated

    Changelog for HubSpot
  • GitHub API

    JavaScript / TypeScript · Python · Go

    83 changes · Upcoming:

    Changelog for GitHub
  • Twilio API

    JavaScript / TypeScript · Python · Go

    730 changes · newest dated

    Changelog for Twilio
  • GitHub MCP server MCP server

    Detected by https://api.githubcopilot.com/mcp, ghcr.io/github/github-mcp-server, github-mcp-server

    81 changes · newest dated

    Changelog for GitHub MCP server
  • Microsoft Learn MCP server MCP server

    Detected by https://learn.microsoft.com/api/mcp

    No changes tracked yet

    Changelog for Microsoft Learn MCP server

Changes tracked

Close

2,216 change events from the providers' own changelogs and release notes.

By provider

  • Twilio 730
  • Stripe 527
  • Shopify 316
  • HubSpot 235
  • OpenAI 141
  • GitHub 83
  • GitHub MCP server 81
  • Gemini 44
  • Mistral 40
  • Claude 19
  • Microsoft Learn MCP server 0

Upcoming

Dated later than today by the provider's own release or API version date.

  1. GitHub CopilotAgentTask.type: type will be removed. Use codingAgentFilter and codingAgentTypeFilter instead.
  2. GitHub User.viewerRelevantRepositories: viewerRelevantRepositories will be removed. Use viewerCopilotChatRepositorySuggestions instead.
  3. Shopify Adding a canceled receive action to the inventory shipments API
  4. Shopify Carrier services will no longer be automatically added to the default shipping profile
  5. Shopify Full-stack capabilities to power app analytics
  6. Shopify Invalid metafield queries now return errors in the GraphQL Admin API
  7. Shopify New translatable boolean field for Metafield GraphQL Object
  8. Shopify Query market hierarchies with the GraphQL Admin API
  9. Shopify `productId`, `title`,`variantSku`, and `variantTitle` fields added to `ExchangeLineItem`
  10. Shopify New paymentInstrumentSendAddEmail mutation emails customers a link to add a payment method for a subscription, order, or draft order
  11. Shopify The `orderCreate` mutation now supports multiple tracking numbers for each fulfillment
  12. Shopify OrderDisplayFulfillmentStatus now returns FULFILLMENT_NOT_REQUIRED for orders with no items to fulfill
  13. Shopify New Pricing audit trail for contextual product variant prices
  14. Shopify Variants now support multiple barcodes
  15. Shopify Removal of ITEM_NOT_STOCKED_AT_LOCATION error
  16. Shopify `DraftOrderDiscountNotAppliedWarning.priceRule` removed in GraphQL Admin API 2026-10
  17. Shopify SubscriptionContractCalculation API now available in early access
  18. Shopify Updating an order's shipping address recalculates taxes as of API version 2026-10
  19. GitHub databaseId will be removed because it does not support 64-bit signed integer identifiers. (2 schema members)
  20. Shopify The `automaticDiscounts` query is removed in API version 2027-01
  21. Shopify Removing deprecated PRIVATE and PUBLIC_READ enums on metaobject definitions
  22. Shopify Removing marketCurrencySettingsUpdate mutation

Latest

  1. Gemini Retirement of embedding-001 on 2025-10-30; replacement: gemini-embedding-2
  2. Gemini Retirement of embedding-2-preview on 2026-08-10; replacement: gemini-embedding-2
  3. Gemini Retirement of embedding-gecko-001 on 2025-10-30; replacement: gemini-embedding-2
  4. Gemini Retirement of gemini-embedding-001 not before 2028-05-14; replacement: gemini-embedding-2
  5. Gemini Retirement of gemini-embedding-exp on 2025-10-30; replacement: gemini-embedding-2
  6. Gemini Retirement of gemini-embedding-exp-03-07 on 2025-10-30; replacement: gemini-embedding-2
  7. Gemini Retirement of text-embedding-004 on 2026-01-14; replacement: gemini-embedding-2
  8. Gemini Retirement of gemini-2.0-flash on 2026-06-01; replacement: gemini-3.6-flash
  9. Gemini Retirement of gemini-2.0-flash-001 on 2026-06-01; replacement: gemini-3.6-flash
  10. Gemini Retirement of gemini-2.0-flash-lite on 2026-06-01; replacement: gemini-3.1-flash-lite
  11. Gemini Retirement of gemini-2.0-flash-lite-001 on 2026-06-01; replacement: gemini-3.1-flash-lite
  12. Gemini Retirement of gemini-2.0-flash-lite-preview on 2025-12-09; replacement: gemini-2.5-flash-lite
  13. Gemini Retirement of gemini-2.0-flash-lite-preview-02-05 on 2025-12-09; replacement: gemini-2.5-flash-lite
  14. Gemini Retirement of gemini-2.0-flash-preview-image-generation on 2025-11-14; replacement: gemini-2.5-flash-image
  15. Gemini Retirement of gemini-2.5-flash-image not before 2026-10-02; replacement: gemini-3.1-flash-image-preview
  16. Gemini Retirement of gemini-2.5-flash-image-preview on 2026-01-15; replacement: gemini-2.5-flash-image
  17. Gemini Retirement of gemini-2.5-flash-lite-preview-09-2025 on 2026-03-31; replacement: gemini-3.1-flash-lite
  18. Gemini Retirement of gemini-2.5-flash-preview-05-20 on 2025-11-18; replacement: gemini-3.6-flash
  19. Gemini Retirement of gemini-2.5-flash-preview-09-25 on 2026-02-17; replacement: gemini-3.6-flash
  20. Gemini Retirement of gemini-2.5-pro-preview-03-25 on 2025-12-02; replacement: gemini-3.1-pro-preview
  21. Gemini Retirement of gemini-2.5-pro-preview-05-06 on 2025-12-02; replacement: gemini-3.1-pro-preview
  22. Gemini Retirement of gemini-2.5-pro-preview-06-05 on 2025-12-02; replacement: gemini-3.1-pro-preview
  23. Gemini Retirement of gemini-3-pro-image-preview not before 2026-06-25; replacement: gemini-3-pro-image
  24. Gemini Retirement of gemini-3-pro-preview on 2026-03-09; replacement: gemini-3.1-pro-preview
  25. Gemini Retirement of gemini-3.1-flash-image-preview not before 2026-06-25; replacement: gemini-3.1-flash-image
  26. Gemini Retirement of gemini-3.1-flash-lite not before 2027-05-07; replacement: gemini-3.5-flash-lite
  27. Gemini Retirement of gemini-3.1-flash-lite-preview on 2026-05-25; replacement: gemini-3.1-flash-lite
  28. Gemini Retirement of gemini-omni-flash-preview not before 2026-09-30; replacement: gemini-omni-1.1-flash
  29. Gemini Retirement of imagen-3.0-generate-002 on 2025-11-10; replacement: imagen-4.0-generate-001
  30. Gemini Retirement of imagen-4.0-fast-generate-001 not before 2026-08-17; replacement: gemini-3.1-flash-image
  31. Gemini Retirement of imagen-4.0-generate-001 not before 2026-08-17; replacement: gemini-3.1-flash-image
  32. Gemini Retirement of imagen-4.0-generate-preview-06-06 on 2026-02-17; replacement: imagen-4.0-generate-001
  33. Gemini Retirement of imagen-4.0-ultra-generate-001 not before 2026-08-17; replacement: gemini-3.1-flash-image
  34. Gemini Retirement of imagen-4.0-ultra-generate-preview-06-06 on 2026-02-17; replacement: imagen-4.0-ultra-generate-001
  35. Gemini Retirement of gemini-2.0-flash-live-001 on 2025-12-09; replacement: gemini-3.8-live
  36. Gemini Retirement of gemini-live-2.5-flash-preview on 2025-12-09; replacement: gemini-3.8-live
  37. Gemini Retirement of antigravity-preview-05-2026 not before 2026-10-05; replacement: antigravity-preview-09-2026
  38. Gemini Retirement of gemini-robotics-er-1.5-preview on 2026-04-30; replacement: gemini-robotics-er-1.6-preview
  39. Gemini Retirement of gemini-robotics-er-1.6-preview not before 2026-08-31; replacement: gemini-robotics-er-2-preview
  40. Gemini Retirement of veo-2.0-generate-001 on 2026-06-30; replacement: veo-3.1-generate-preview
  41. Gemini Retirement of veo-3.0-fast-generate-001 not before 2026-06-30; replacement: veo-3.1-fast-generate-preview
  42. Gemini Retirement of veo-3.0-fast-generate-preview on 2025-11-12; replacement: veo-3.1-fast-generate-preview
  43. Gemini Retirement of veo-3.0-generate-001 not before 2026-06-30; replacement: veo-3.1-generate-preview
  44. Gemini Retirement of veo-3.0-generate-preview on 2025-11-12; replacement: veo-3.1-generate-preview
  45. GitHub MCP server Tool update_issue_comment added
  46. OpenAI Retirement of gpt-5.4-cyber on 2026-10-01; replacement: gpt-5.6-cyber
  47. Twilio Breaking change: Removed the deprecated `CSV` and `DATASET` values from the `DataMappingType` enum.
  48. Twilio Added 1 new path(s): `/preview/Snapshots/{cohortSnapshotId}/Operations` (AdminListSnapshotOperations)
  49. Twilio Added 5 new path(s): `/preview/Cohorts` (AdminListCohorts)
  50. Twilio Added the standard `pageSize`/`pageToken` query parameters to `ListAdminSnapshotOperations`, the only list operation missing them.

Languages

Close

3 languages, with the SDK packages Upseam recognizes in each.

JavaScript / TypeScript (Node.js) 9 providers

  • Stripe

    stripe

  • Shopify

    @shopify/shopify-api @shopify/shopify-app-react-router @shopify/shopify-app-remix @shopify/admin-api-client

  • OpenAI

    openai @ai-sdk/openai @langchain/openai

  • Claude

    @anthropic-ai/sdk @ai-sdk/anthropic @langchain/anthropic

  • Gemini

    @google/genai @google/generative-ai @ai-sdk/google @langchain/google-genai

  • Mistral

    @mistralai/mistralai @ai-sdk/mistral @langchain/mistralai

  • HubSpot

    @hubspot/sdk @hubspot/api-client

  • GitHub

    octokit @octokit/rest @octokit/core @octokit/graphql @octokit/request

  • Twilio

    twilio

Python 9 providers

  • Stripe

    stripe

  • Shopify

    ShopifyAPI

  • OpenAI

    openai langchain-openai

  • Claude

    anthropic langchain-anthropic

  • Gemini

    google-genai google-generativeai langchain-google-genai

  • Mistral

    mistralai langchain-mistralai

  • HubSpot

    hubspot-sdk hubspot-api-client

  • GitHub

    PyGithub

  • Twilio

    twilio

Go 6 providers

  • Stripe

    github.com/stripe/stripe-go

  • OpenAI

    github.com/openai/openai-go

  • Claude

    github.com/anthropics/anthropic-sdk-go

  • Gemini

    google.golang.org/genai github.com/google/generative-ai-go

  • GitHub

    github.com/octokit/go-sdk

  • Twilio

    github.com/twilio/twilio-go

Trademarks belong to their owners; no endorsement implied.

01 / Breaking changes

Removed, renamed, retyped.

Mechanical changes get a fix. Behavior changes are flagged for you, never guessed.

02 / New capabilities

Retired model? Here is its successor.

A pull request to the named replacement, with the parameters to review. Pinned API versions rise to what your SDK supports.

03 / With Dependabot

Dependabot bumps it. Upseam fixes what breaks.

When a Dependabot or Renovate bump breaks code: one PR with both.

04 / Your own contracts

Backend changes, frontend PRs.

Change an OpenAPI or GraphQL contract; every consumer gets a PR.

04 / Guardrails

Upseam opens pull requests.
You stay in charge.

Six rules, on the pull request and around it.

  1. 01 / Code

    Your code stays yours.

    We never store your code.

  2. 02 / Key

    Your model, your key.

    Billed by your provider.

  3. 03 / Diff

    Small diffs.

    Edits stay next to the matched lines.

  4. 04 / Checks

    Your CI decides.

  5. 05 / Merge

    Upseam never merges.

  6. 06 / Close

    Closed means closed.

    It won’t reopen.

05 / Connect

Your model,
or your agent.

Upseam finds the change and the code it touches. The fix comes from the model you choose or from the coding agent you already use.

01 / Via API

Via API

Nothing to add to your repo: paste your key on the settings page and enter the model id.

  • Anthropic
  • OpenAI
  • OpenAI-compatible

Model is the id as your vendor names it. Upseam keeps no list of models.

Or run it in your own Actions (after the first upseam/action release)

.github/workflows/upseam-generate.ymlYAML

- uses: upseam/action@<40-character commit SHA> # pin a release
  with:
    vendor: anthropic
    model: <model id>
  env:
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
Learn more

02 / Your agent

Your agent, directly

Claude Code, Codex, OpenCode, Hermes Agent or OpenClaw gets the task and edits the files.

1. Add the setting.github/upseam.yml

agent: claude-code

2. Add the workflow.github/workflows/

name: upseam-agent
on:
  repository_dispatch:
    types: [upseam-agent]
permissions: {}
concurrency: upseam-agent-${{ github.event.client_payload.group }}
env:
  FILES: ${{ toJSON(github.event.client_payload.files) }}
jobs:
  agent:
    if: github.event.client_payload.runner == 'claude-code'
    runs-on: ubuntu-latest
    timeout-minutes: 30
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
        with:
          ref: ${{ github.event.client_payload.sha }}
          persist-credentials: false
      - uses: anthropics/claude-code-action@8cf3482550831fb35a4fc3fbf7ca139cf8028b4c # v1.0.233
        with:
          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
          github_token: ${{ github.token }}
          allowed_bots: ${{ github.actor }}
          prompt: ${{ github.event.client_payload.task }}
          settings: '{"permissions":{"blockReadsOutsideWorkingDirectories":true,"deny":["Read(//proc/**)","Read(//sys/**)","Read(//etc/**)","Read(//tmp/**)","Read(./.git/**)","Edit(./.git/**)","Edit(./.github/**)","Edit(//home/runner/work/_actions/**)","Edit(//home/runner/work/_temp/**)","Edit(//tmp/**)"]}}'
          claude_args: --allowedTools Read,Edit,Glob,Grep --disallowedTools Bash,WebFetch,WebSearch --max-turns 30
      - run: rm -rf .upseam-check
      - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
        with:
          ref: ${{ github.event.client_payload.sha }}
          path: .upseam-check
          persist-credentials: false
      - name: Copy the listed files into the fresh checkout
        run: |
          set -euo pipefail
          total=0
          while IFS= read -r -d '' f; do
            case "$f" in "" | /* | ../* | */../* | */.. | .git/* | */.git/*) echo "::error::Bad path in files."; exit 1 ;; esac
            if [ -f "$f" ] && [ ! -L "$f" ] && [ -f ".upseam-check/$f" ] && [ ! -L ".upseam-check/$f" ]; then
              total=$((total + $(stat -c %s -- "$f")))
              [ "$total" -le 1048576 ] || { echo "::error::The edits are over 1 MB."; exit 1; }
              cp -- "$f" ".upseam-check/$f"
            fi
          done < <(jq -j '.[] | ., "\u0000"' <<<"$FILES")
      - uses: upseam/action@<40-character SHA> # v0.x, after the first release
        with:
          path: .upseam-check
          check-only: true
      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: upseam-edits
          path: ${{ runner.temp }}/upseam-edits
          include-hidden-files: true
          if-no-files-found: error
          retention-days: 1
  push:
    needs: agent
    runs-on: ubuntu-latest
    timeout-minutes: 15
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
        with:
          ref: ${{ github.event.client_payload.sha }}
          persist-credentials: false
      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: upseam-edits
          path: ${{ runner.temp }}/upseam-edits
      - name: Copy the listed files over the checkout
        run: |
          set -euo pipefail
          src="$RUNNER_TEMP/upseam-edits"
          total=0
          while IFS= read -r -d '' f; do
            case "$f" in "" | /* | ../* | */../* | */.. | .git/* | */.git/*) echo "::error::Bad path in files."; exit 1 ;; esac
            if [ -f "$src/$f" ] && [ ! -L "$src/$f" ] && [ -f "$f" ] && [ ! -L "$f" ]; then
              total=$((total + $(stat -c %s -- "$src/$f")))
              [ "$total" -le 1048576 ] || { echo "::error::The edits are over 1 MB."; exit 1; }
              cp -- "$src/$f" "$f"
            fi
          done < <(jq -j '.[] | ., "\u0000"' <<<"$FILES")
      - uses: upseam/action@<40-character SHA> # v0.x, after the first release

What happens

  1. Upseam sends the upseam-agent task: no file contents, no key, no token.
  2. Claude Code edits the listed files in your Actions.
  3. upseam/action checks the edits and pushes one commit; Upseam checks it again and opens the pull request.

Claude Code gets only the Read, Edit, Glob and Grep tools; Bash, WebFetch and WebSearch are disallowed.

1. Add the setting.github/upseam.yml

agent: codex

2. Add the workflow.github/workflows/

Replace the if and the Claude Code step of the agent job with:

    if: github.event.client_payload.runner == 'codex'
    # ...
      - uses: openai/codex-action@86365089eb2b84e0a8fb0717b304f8bdcb13b20e # v1.12
        with:
          openai-api-key: ${{ secrets.OPENAI_API_KEY }}
          prompt: ${{ github.event.client_payload.task }}
          permission-profile: ":workspace"
          allow-bot-users: ${{ github.actor }}

What happens

  1. Upseam sends the upseam-agent task: no file contents, no key, no token.
  2. Codex edits the listed files in your Actions.
  3. upseam/action checks the edits and pushes one commit; Upseam checks it again and opens the pull request.

Codex runs in the :workspace permission profile, and the action removes sudo before Codex runs.

1. Add the setting.github/upseam.yml

agent: opencode

2. Add the workflow.github/workflows/

Set MODEL to provider/model, and replace the agent job with this one; the rest of the workflow stays the same:

jobs:
  agent:
    if: github.event.client_payload.runner == 'opencode'
    runs-on: ubuntu-latest
    timeout-minutes: 30
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
        with:
          ref: ${{ github.event.client_payload.sha }}
          persist-credentials: false
      - name: Install OpenCode 1.18.32 and ripgrep 15.1.0
        run: |
          set -euo pipefail
          [ -z "${XDG_DATA_HOME:-}" ] && [ "$HOME" = /home/runner ] || { echo "::error::The OpenCode job needs a GitHub-hosted Ubuntu runner without XDG_DATA_HOME."; exit 1; }
          dir="$RUNNER_TEMP/opencode"
          mkdir -p "$dir"
          curl -fsSL --proto '=https' -o "$dir/opencode.tgz" https://registry.npmjs.org/opencode-linux-x64/-/opencode-linux-x64-1.18.32.tgz
          echo "0886adbe8ca2f15e5ae7ac72c3a6549ec2b107dc2a60866a7cd7aa50d5a10416386843409bdd3b2cfd315c0c0bead4747186ea365dbe0efc7c98ea972316c379  $dir/opencode.tgz" | sha512sum -c -
          tar -xzf "$dir/opencode.tgz" -C "$dir" package/bin/opencode
          curl -fsSL --proto '=https' -o "$dir/rg.tgz" https://github.com/BurntSushi/ripgrep/releases/download/15.1.0/ripgrep-15.1.0-x86_64-unknown-linux-musl.tar.gz
          echo "1c9297be4a084eea7ecaedf93eb03d058d6faae29bbc57ecdaf5063921491599  $dir/rg.tgz" | sha256sum -c -
          bin="${XDG_CACHE_HOME:-$HOME/.cache}/opencode/bin"
          mkdir -p "$bin"
          tar -xzf "$dir/rg.tgz" -C "$bin" --strip-components=1 ripgrep-15.1.0-x86_64-unknown-linux-musl/rg
          config="${XDG_CONFIG_HOME:-$HOME/.config}/opencode"
          mkdir -p "$config/node_modules"
          echo '{"dependencies":{"@opencode-ai/plugin":"1.18.32"}}' > "$config/package.json"
          echo '{"lockfileVersion":3,"packages":{"":{"dependencies":{"@opencode-ai/plugin":"1.18.32"}}}}' > "$config/package-lock.json"
      - name: Run OpenCode
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          MODEL: anthropic/<model id>
          TASK: ${{ github.event.client_payload.task }}
          OPENCODE_CONFIG_CONTENT: '{"autoupdate":false,"share":"disabled","snapshot":false,"lsp":false,"formatter":false,"permission":{"*":"deny","read":{"*":"allow","*.env":"deny","*.env.*":"deny",".git":"deny",".git/*":"deny"},"glob":"allow","grep":"allow","edit":{"*":"allow",".git":"deny",".git/*":"deny",".github/*":"deny","opencode.json":"deny","opencode.jsonc":"deny",".opencode/*":"deny"},"bash":"deny","webfetch":"deny","websearch":"deny","task":"deny","skill":"deny","question":"deny","lsp":"deny","external_directory":{"*":"deny","/home/runner/.local/share/opencode/tool-output/*":"deny"},"doom_loop":"deny"},"agent":{"build":{"steps":30}}}'
          OPENCODE_DISABLE_PROJECT_CONFIG: "1"
          OPENCODE_DISABLE_AUTOUPDATE: "1"
          OPENCODE_DISABLE_MODELS_FETCH: "1"
          OPENCODE_DISABLE_LSP_DOWNLOAD: "1"
          OPENCODE_DISABLE_DEFAULT_PLUGINS: "1"
          OPENCODE_DISABLE_CLAUDE_CODE: "1"
        run: |
          set -euo pipefail
          rm -rf opencode.json opencode.jsonc .opencode
          find . -path ./.git -prune -o -type l -exec rm -f -- {} +
          printf '%s' "$TASK" | "$RUNNER_TEMP/opencode/package/bin/opencode" --pure run --model "$MODEL" --agent build
      - run: rm -rf .upseam-check
      - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
        with:
          ref: ${{ github.event.client_payload.sha }}
          path: .upseam-check
          persist-credentials: false
      - name: Copy the listed files into the fresh checkout
        run: |
          set -euo pipefail
          total=0
          while IFS= read -r -d '' f; do
            case "$f" in "" | /* | ../* | */../* | */.. | .git/* | */.git/*) echo "::error::Bad path in files."; exit 1 ;; esac
            if [ -f "$f" ] && [ ! -L "$f" ] && [ -f ".upseam-check/$f" ] && [ ! -L ".upseam-check/$f" ]; then
              total=$((total + $(stat -c %s -- "$f")))
              [ "$total" -le 1048576 ] || { echo "::error::The edits are over 1 MB."; exit 1; }
              cp -- "$f" ".upseam-check/$f"
            fi
          done < <(jq -j '.[] | ., "\u0000"' <<<"$FILES")
      - uses: upseam/action@<40-character SHA> # v0.x, after the first release
        with:
          path: .upseam-check
          check-only: true
      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: upseam-edits
          path: ${{ runner.temp }}/upseam-edits
          include-hidden-files: true
          if-no-files-found: error
          retention-days: 1

What happens

  1. Upseam sends the upseam-agent task: no file contents, no key, no token.
  2. OpenCode edits the listed files in your Actions.
  3. upseam/action checks the edits and pushes one commit; Upseam checks it again and opens the pull request.

OpenCode may only read, search and edit files in the repository: no shell, no web access, no subagents.

1. Add the setting.github/upseam.yml

agent: hermes

2. Add the workflow.github/workflows/

Create the Actions secret ANTHROPIC_API_KEY with an Anthropic API key and replace the agent job of the workflow with this one; the push job stays the same.

jobs:
  agent:
    if: github.event.client_payload.runner == 'hermes'
    runs-on: ubuntu-latest
    timeout-minutes: 30
    permissions:
      contents: read
    env:
      HERMES_SHA: f97608f178d1ffeca59860195ab7da295f7c8e5f
      IMAGE: ghcr.io/astral-sh/uv:0.11.6-python3.13-trixie@sha256:b3c543b6c4f23a5f2df22866bd7857e5d304b67a564f4feab6ac22044dde719b
      PROVIDER: anthropic
      MODEL: claude-sonnet-4-6
      PROVIDER_HOST: api.anthropic.com
    steps:
      - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
        with:
          ref: ${{ github.event.client_payload.sha }}
          path: .upseam-check
          persist-credentials: false
      - name: Copy the listed files into the sandbox directory
        run: |
          set -euo pipefail
          work="$RUNNER_TEMP/upseam-work"
          mkdir -p "$work"
          root="$(realpath -- .upseam-check)"
          while IFS= read -r -d '' f; do
            case "$f" in "" | /* | ../* | */../* | */.. | .git/* | */.git/*) echo "::error::Bad path in files."; exit 1 ;; esac
            if [ -f ".upseam-check/$f" ] && [ "$(realpath -e -- ".upseam-check/$f")" = "$root/$f" ]; then
              mkdir -p -- "$work/$(dirname -- "$f")"
              cp -- ".upseam-check/$f" "$work/$f"
            fi
          done < <(jq -j '.[] | ., "\u0000"' <<<"$FILES")
          chmod -R a+rwX "$work"
      - name: Build Hermes Agent from its commit
        run: |
          set -euo pipefail
          src="$RUNNER_TEMP/hermes-src"
          git init -q "$src"
          git -C "$src" fetch -q --depth 1 https://github.com/NousResearch/hermes-agent.git "$HERMES_SHA"
          git -C "$src" -c advice.detachedHead=false checkout -q FETCH_HEAD
          [ "$(git -C "$src" rev-parse HEAD)" = "$HERMES_SHA" ]
          docker build -q -t upseam-hermes --build-arg IMAGE -f - "$src" <<'DOCKERFILE'
          ARG IMAGE
          FROM $IMAGE
          WORKDIR /opt/hermes
          COPY . .
          RUN uv sync --locked --no-dev --no-install-project --no-build --no-python-downloads --python 3.13 --extra anthropic && printf 'docker\n' > .install_method
          ENV PATH=/opt/hermes/.venv/bin:$PATH PYTHONDONTWRITEBYTECODE=1
          DOCKERFILE
      - name: Start the egress proxy
        env:
          PROXY_PY: |
            import asyncio, os, time
            ALLOW = set(os.environ["ALLOW"].split(","))
            SLOTS = asyncio.Semaphore(32)
            IDLE = 300
            async def pipe(r, w, seen):
                try:
                    while data := await r.read(65536):
                        seen[0] = time.monotonic()
                        w.write(data)
                        await w.drain()
                    w.close()
                except OSError:
                    w.transport.abort()
            async def handle(r, w):
                if SLOTS.locked():
                    w.close()
                    return
                async with SLOTS:
                    try:
                        head = await asyncio.wait_for(r.readuntil(b"\r\n\r\n"), 10)
                        method, target, _ = head.split(b"\r\n")[0].split(b" ")
                        host, port = target.decode("ascii").rsplit(":", 1)
                        if method != b"CONNECT" or port != "443" or host not in ALLOW:
                            raise PermissionError
                        ur, uw = await asyncio.wait_for(asyncio.open_connection(host, 443), 10)
                    except Exception:
                        print("deny", flush=True)
                        w.write(b"HTTP/1.1 403 Forbidden\r\n\r\n")
                        w.close()
                        return
                    print("allow", host, flush=True)
                    w.write(b"HTTP/1.1 200 Connection established\r\n\r\n")
                    seen = [time.monotonic()]
                    tunnel = asyncio.gather(pipe(r, uw, seen), pipe(ur, w, seen))
                    while not tunnel.done():
                        await asyncio.wait([tunnel], timeout=10)
                        if time.monotonic() - seen[0] > IDLE:
                            uw.transport.abort()
                            w.transport.abort()
            async def main():
                server = await asyncio.start_server(handle, "0.0.0.0", 3128)
                await server.serve_forever()
            asyncio.run(main())
        run: |
          set -euo pipefail
          docker network create --internal -o com.docker.network.bridge.gateway_mode_ipv4=isolated upseam-sandbox
          docker network create upseam-egress
          docker run -d --name upseam-proxy --network upseam-egress \
            --read-only --cap-drop=ALL --security-opt no-new-privileges \
            --user 65534:65534 --pids-limit 64 --memory 128m --memory-swap 128m \
            -e ALLOW="$PROVIDER_HOST" \
            "$IMAGE" python -c "$PROXY_PY"
          docker network connect upseam-sandbox upseam-proxy
      - name: Run Hermes Agent in the sandbox
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          TASK: ${{ github.event.client_payload.task }}
        run: |
          set -euo pipefail
          printf '%s' "$TASK" | docker run --rm -i --network upseam-sandbox \
            --read-only --tmpfs /tmp:rw,nosuid,nodev,size=512m \
            --cap-drop=ALL --security-opt no-new-privileges \
            --user 65534:65534 --pids-limit 256 --memory 2g --memory-swap 2g \
            -v "$RUNNER_TEMP/upseam-work:/work" -w /work \
            -e HOME=/tmp -e HERMES_HOME=/tmp/hermes \
            -e HTTPS_PROXY=http://upseam-proxy:3128 \
            -e HERMES_WRITE_SAFE_ROOT=/work \
            -e TIRITH_ENABLED=0 -e HERMES_DISABLE_LAZY_INSTALLS=1 \
            -e ANTHROPIC_API_KEY \
            upseam-hermes \
            python /opt/hermes/hermes chat --safe-mode -Q --query-file - -t file \
            --provider "$PROVIDER" --model "$MODEL" --max-turns 30 >/dev/null 2>&1 || status=$?
          echo "Agent exit code: ${status:-0}"
          exit "${status:-0}"
      - name: Show the proxy log and remove the sandbox
        if: always()
        run: |
          docker logs upseam-proxy || true
          docker rm -f upseam-proxy || true
          docker network rm upseam-sandbox upseam-egress || true
      - name: Copy the edits into the checkout
        run: |
          set -euo pipefail
          work="$(realpath -- "$RUNNER_TEMP/upseam-work")"
          root="$(realpath -- .upseam-check)"
          total=0
          while IFS= read -r -d '' f; do
            case "$f" in "" | /* | ../* | */../* | */.. | .git/* | */.git/*) echo "::error::Bad path in files."; exit 1 ;; esac
            if [ -f "$work/$f" ] && [ "$(realpath -e -- "$work/$f")" = "$work/$f" ] && [ -f ".upseam-check/$f" ] && [ ! -L ".upseam-check/$f" ] && [ "$(realpath -e -- ".upseam-check/$f")" = "$root/$f" ]; then
              total=$((total + $(stat -c %s -- "$work/$f")))
              [ "$total" -le 1048576 ] || { echo "::error::The edits are over 1 MB."; exit 1; }
              cp -- "$work/$f" ".upseam-check/$f"
            fi
          done < <(jq -j '.[] | ., "\u0000"' <<<"$FILES")
      - uses: upseam/action@<40-character SHA> # v0.x, after the first release
        with:
          path: .upseam-check
          check-only: true
      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: upseam-edits
          path: ${{ runner.temp }}/upseam-edits
          include-hidden-files: true
          if-no-files-found: error
          retention-days: 1

What happens

  1. Upseam sends the upseam-agent task: no file contents, no key, no token.
  2. Hermes Agent edits the listed files in your Actions.
  3. upseam/action checks the edits and pushes one commit; Upseam checks it again and opens the pull request.

1. Add the setting.github/upseam.yml

agent: openclaw

2. Add the workflow.github/workflows/

Create the Actions secret ANTHROPIC_API_KEY and replace the agent job with this one; the push job stays the same.

jobs:
  agent:
    if: github.event.client_payload.runner == 'openclaw'
    runs-on: ubuntu-latest
    timeout-minutes: 30
    permissions:
      contents: read
    env:
      OPENCLAW_VERSION: 2026.9.6
      OPENCLAW_INTEGRITY: sha512-Ie0kyQSCVfFqixsgVg39vevUDq01Ch5u3+7Yu5Y3qARczmdAe+lzp8bVnO9925rHiW/+CFp70zfORCyPmCH31g==
      IMAGE: node:24.21.0-bookworm-slim@sha256:0e0ff40c39bc087845bfb27465a0df4ea419520094bc35842ff83dd8cbe6f9b6
      PROXY_IMAGE: ghcr.io/astral-sh/uv:0.11.6-python3.13-trixie@sha256:b3c543b6c4f23a5f2df22866bd7857e5d304b67a564f4feab6ac22044dde719b
      MODEL: anthropic/claude-sonnet-4-6
      PROVIDER_HOST: api.anthropic.com
    steps:
      - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
        with:
          ref: ${{ github.event.client_payload.sha }}
          path: .upseam-check
          persist-credentials: false
      - name: Copy the listed files into the sandbox directory
        run: |
          set -euo pipefail
          work="$RUNNER_TEMP/upseam-work"
          mkdir -p "$work"
          root="$(realpath -- .upseam-check)"
          while IFS= read -r -d '' f; do
            case "$f" in "" | /* | ../* | */../* | */.. | .git/* | */.git/*) echo "::error::Bad path in files."; exit 1 ;; esac
            if [ -f ".upseam-check/$f" ] && [ "$(realpath -e -- ".upseam-check/$f")" = "$root/$f" ]; then
              mkdir -p -- "$work/$(dirname -- "$f")"
              cp -- ".upseam-check/$f" "$work/$f"
            fi
          done < <(jq -j '.[] | ., "\u0000"' <<<"$FILES")
          chmod -R a+rwX "$work"
      - name: Build OpenClaw from its npm release
        env:
          OPENCLAW_CONFIG: |
            {
              update: { checkOnStart: false },
              env: { shellEnv: { enabled: false } },
              agents: {
                defaults: {
                  sandbox: { mode: "off" },
                  skipBootstrap: true,
                  contextInjection: "never",
                  skills: [],
                },
              },
              tools: {
                profile: "coding",
                allow: ["ls", "read", "write", "edit", "apply_patch"],
                deny: ["group:runtime", "group:web", "group:ui", "group:sessions", "group:memory", "group:automation", "group:messaging", "group:nodes", "group:agents", "group:media", "group:openclaw", "group:plugins", "bundle-mcp"],
                fs: { workspaceOnly: true },
                exec: { mode: "deny", applyPatch: { workspaceOnly: true } },
                elevated: { enabled: false },
                codeMode: false,
              },
            }
        run: |
          set -euo pipefail
          ctx="$RUNNER_TEMP/openclaw-image"
          mkdir -p "$ctx"
          jq -n --arg v "$OPENCLAW_VERSION" '{private: true, dependencies: {openclaw: $v}}' > "$ctx/package.json"
          printf '%s' "$OPENCLAW_CONFIG" > "$ctx/openclaw.json5"
          docker build -q -t upseam-openclaw --build-arg IMAGE --build-arg OPENCLAW_INTEGRITY -f - "$ctx" <<'DOCKERFILE'
          ARG IMAGE
          FROM $IMAGE
          ARG OPENCLAW_INTEGRITY
          WORKDIR /opt/openclaw
          COPY package.json openclaw.json5 ./
          RUN npm install --ignore-scripts --no-audit --no-fund --before=2026-09-24T00:00:00Z && [ "$(node -p 'require("./node_modules/.package-lock.json").packages["node_modules/openclaw"].integrity')" = "$OPENCLAW_INTEGRITY" ] && OPENCLAW_DISABLE_BUNDLED_PLUGIN_POSTINSTALL=1 node node_modules/openclaw/scripts/postinstall-bundled-plugins.mjs && test ! -e node_modules/openclaw/.openclaw-lifecycle-pending
          ENV PATH=/opt/openclaw/node_modules/.bin:$PATH
          DOCKERFILE
      - name: Start the egress proxy
        env:
          PROXY_PY: |
            import asyncio, os, time
            ALLOW = set(os.environ["ALLOW"].split(","))
            SLOTS = asyncio.Semaphore(32)
            IDLE = 300
            async def pipe(r, w, seen):
                try:
                    while data := await r.read(65536):
                        seen[0] = time.monotonic()
                        w.write(data)
                        await w.drain()
                    w.close()
                except OSError:
                    w.transport.abort()
            async def handle(r, w):
                if SLOTS.locked():
                    w.close()
                    return
                async with SLOTS:
                    try:
                        head = await asyncio.wait_for(r.readuntil(b"\r\n\r\n"), 10)
                        method, target, _ = head.split(b"\r\n")[0].split(b" ")
                        host, port = target.decode("ascii").rsplit(":", 1)
                        if method != b"CONNECT" or port != "443" or host not in ALLOW:
                            raise PermissionError
                        ur, uw = await asyncio.wait_for(asyncio.open_connection(host, 443), 10)
                    except Exception:
                        print("deny", flush=True)
                        w.write(b"HTTP/1.1 403 Forbidden\r\n\r\n")
                        w.close()
                        return
                    print("allow", host, flush=True)
                    w.write(b"HTTP/1.1 200 Connection established\r\n\r\n")
                    seen = [time.monotonic()]
                    tunnel = asyncio.gather(pipe(r, uw, seen), pipe(ur, w, seen))
                    while not tunnel.done():
                        await asyncio.wait([tunnel], timeout=10)
                        if time.monotonic() - seen[0] > IDLE:
                            uw.transport.abort()
                            w.transport.abort()
            async def main():
                server = await asyncio.start_server(handle, "0.0.0.0", 3128)
                await server.serve_forever()
            asyncio.run(main())
        run: |
          set -euo pipefail
          docker network create --internal -o com.docker.network.bridge.gateway_mode_ipv4=isolated upseam-sandbox
          docker network create upseam-egress
          docker run -d --name upseam-proxy --network upseam-egress \
            --read-only --cap-drop=ALL --security-opt no-new-privileges \
            --user 65534:65534 --pids-limit 64 --memory 128m --memory-swap 128m \
            -e ALLOW="$PROVIDER_HOST" \
            "$PROXY_IMAGE" python -c "$PROXY_PY"
          docker network connect upseam-sandbox upseam-proxy
      - name: Run OpenClaw in the sandbox
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          TASK: ${{ github.event.client_payload.task }}
        run: |
          set -euo pipefail
          printf '%s' "$TASK" | docker run --rm -i --network upseam-sandbox \
            --read-only --tmpfs /tmp:rw,nosuid,nodev,size=512m \
            --cap-drop=ALL --security-opt no-new-privileges \
            --user 65534:65534 --pids-limit 256 --memory 2g --memory-swap 2g \
            -v "$RUNNER_TEMP/upseam-work:/work" -w /tmp \
            -e HOME=/tmp -e CI=true -e DO_NOT_TRACK=1 \
            -e OPENCLAW_NO_AUTO_UPDATE=1 -e OPENCLAW_TELEMETRY=0 \
            -e HTTPS_PROXY=http://upseam-proxy:3128 \
            -e HTTP_PROXY=http://upseam-proxy:3128 \
            -e ANTHROPIC_API_KEY \
            upseam-openclaw \
            openclaw agent exec --config /opt/openclaw/openclaw.json5 \
            --cwd /work --message-file - --model "$MODEL" --timeout 900 >/dev/null 2>&1 || status=$?
          echo "Agent exit code: ${status:-0}"
          exit "${status:-0}"
      - name: Show the proxy log and remove the sandbox
        if: always()
        run: |
          docker logs upseam-proxy || true
          docker rm -f upseam-proxy || true
          docker network rm upseam-sandbox upseam-egress || true
      - name: Copy the edits into the checkout
        run: |
          set -euo pipefail
          work="$(realpath -- "$RUNNER_TEMP/upseam-work")"
          root="$(realpath -- .upseam-check)"
          total=0
          while IFS= read -r -d '' f; do
            case "$f" in "" | /* | ../* | */../* | */.. | .git/* | */.git/*) echo "::error::Bad path in files."; exit 1 ;; esac
            if [ -f "$work/$f" ] && [ "$(realpath -e -- "$work/$f")" = "$work/$f" ] && [ -f ".upseam-check/$f" ] && [ ! -L ".upseam-check/$f" ] && [ "$(realpath -e -- ".upseam-check/$f")" = "$root/$f" ]; then
              total=$((total + $(stat -c %s -- "$work/$f")))
              [ "$total" -le 1048576 ] || { echo "::error::The edits are over 1 MB."; exit 1; }
              cp -- "$work/$f" ".upseam-check/$f"
            fi
          done < <(jq -j '.[] | ., "\u0000"' <<<"$FILES")
      - uses: upseam/action@<40-character SHA> # v0.x, after the first release
        with:
          path: .upseam-check
          check-only: true
      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: upseam-edits
          path: ${{ runner.temp }}/upseam-edits
          include-hidden-files: true
          if-no-files-found: error
          retention-days: 1

What happens

  1. Upseam sends the upseam-agent task: no file contents, no key, no token.
  2. OpenClaw edits the listed files in your Actions.
  3. upseam/action checks the edits and pushes one commit; Upseam checks it again and opens the pull request.

Claude Code, Codex, OpenCode, Hermes Agent and OpenClaw are supported; the workflow runs once the first upseam/action release is out. Learn more

Trademarks belong to their owners; no endorsement implied.

06 / Join the beta

Your APIs will change. Your code can keep up.

Free during the beta.

Try for free

The beta opens soon — we’ll email you the install link.

Opens your email app; nothing is sent or stored by this site.