GitHub Action for IronBee CLI — Verify and fix code changes using IronBee DevTools (browser, backend, and Node.js modes) with Claude Code.
IronBee.Action.-.Demo.compressed.mp4
IronBee Action verifies your code changes against a running application, fixes what it finds, and reports on the pull request.
The verification itself can run in one of two places. Everything after it — the verdict, the fix, the commit, the report — is the same either way.
| Platform | Local | |
|---|---|---|
| Where the agent runs | IronBee's infrastructure | this runner |
| Model cost | IronBee's | your Anthropic account |
| Runner setup | none — no Chromium, no DevTools install | Chromium, DevTools, MCP, ~2 minutes |
| Needs an Anthropic credential | only to fix what it finds | yes |
| Evidence | in the IronBee Console | Console and a workflow artifact |
| Private repositories | needs the IronBee GitHub App installed | works with any checkout |
verification_mode picks between them and defaults to auto:
- the repository is public → platform. A public repository can be checked out without the GitHub App.
- the repository is private and an Anthropic credential is set → local. A private repository needs the App, and whether it is installed is the one thing the action cannot check for free — so it uses the engine it knows can run. If the App does cover your repository, set
verification_mode: platform. - the repository is private with no Anthropic credential → platform, because it is the only engine available. If the App is missing the run fails saying so — set
ironbee_bind_repository: falseto verify the application without a checkout instead.
- Resolve the plan — mode, target, changeset — and refuse anything that cannot work, before installing anything
- Verify: start a platform job and follow it, or run the agent here
- Fix what a failing verdict reported, when
fixis on and an Anthropic credential is set - Commit the fixes — to the PR branch on a pull request, as a new PR otherwise
- Report on the pull request, with a link to the run in the IronBee Console
name: IronBee Verification
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize]
permissions:
contents: write
pull-requests: write
issues: write
jobs:
verify:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- uses: ironbee-ai/ironbee-action@v1
with:
ironbee_api_key: ${{ secrets.IRONBEE_API_KEY }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}The action adapts its behavior based on the trigger event:
| Trigger | Verification Mode | Fix Behavior |
|---|---|---|
pull_request |
Diff-based — verifies changed pages | Commits fixes to PR branch, posts verification report comment |
push (main/master) |
Diff-based — verifies changed pages | Creates fix PR automatically if issues found |
workflow_dispatch |
Full — verifies entire application | Creates fix PR automatically if issues found |
schedule |
Full — verifies entire application | Creates fix PR automatically if issues found |
issue_comment |
Diff-based — verifies the pull request the comment is on (never switched off by verification_auto) |
Commits fixes to the PR branch only when the comment asks, posts the report comment |
on:
pull_request:
types: [opened, synchronize]Reviews the PR diff, verifies affected pages, and posts a verification report comment. Fixes are committed directly to the PR branch.
on:
push:
branches: [main]Reviews the pushed commit diff and verifies affected pages. If issues are found, creates a new branch with fixes and opens a PR.
on:
schedule:
- cron: '0 9 * * 1' # Every Monday at 09:00 UTCRuns a full application verification on a schedule. No diff — tests the entire application. Creates a fix PR if issues are found.
on:
issue_comment:
types: [created]
concurrency:
group: ironbee-${{ github.event.issue.number }}
cancel-in-progress: true
jobs:
verify:
# WHO may run it is decided here, not by the action. An `issue_comment`
# event fires for anyone who can leave a comment and runs in the BASE
# repository with its secrets — so this condition is the security boundary.
if: >-
github.event.issue.pull_request &&
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) &&
startsWith(github.event.comment.body, '/ironbee-verify')
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
# `issue_comment` runs on the DEFAULT branch and `github.sha` points there,
# not at the pull request — the head has to be checked out explicitly.
- uses: actions/checkout@v4
with:
ref: refs/pull/${{ github.event.issue.number }}/head
fetch-depth: 0
- uses: ironbee-ai/ironbee-action@v0
with:
...Then, on any pull request:
/ironbee-verify
/ironbee-verify --fix
/ironbee-verify check the coupon flow end to end
/ironbee-verify --fix the totals are wrong when a coupon is applied
Options are marked with --, so scanning stops at the first token that is not
one and everything after it is the instruction for the agent. That is what makes
/ironbee-verify fix the cart total a prompt rather than a run that commits: a
bare fix is an ordinary English verb, and reading it as a flag would apply
changes nobody asked for. A multi-line comment works too — the whole body below
the command line is the prompt.
The command must be the first non-quoted line of the comment, so a comment that merely discusses it, or a reply that quotes it, starts nothing.
--fix is opt-in per comment and bounded by verification_apply_fix: a
repository that switched fixing off cannot have it switched back on from a
comment. Unknown options are answered with a usage reply rather than read as
prose, and that run ends red — a composite action cannot stop halfway, so the
alternative would be verifying something other than what was asked for.
The comment gets an 👀 reaction when the run starts and 👍 / 👎 when it ends —
an issue_comment run does not appear in the pull request's checks, so without
them there is nothing to tell "it started" from "nothing listened".
Fork pull requests are refused on this event, and for the opposite reason to the usual one: a comment run holds the base repository's secrets, so building and starting a fork's code under it would hand them to whoever opened it.
To have only the comment command (and a manual dispatch) verify anything, while a push or a pull request does nothing. There are two ways, and they cost different things.
In your workflow's if: — nothing runs at all. GitHub evaluates this before
a runner is assigned, so the job is skipped outright: no runner minutes, no
checkout, no builds.
jobs:
verify:
if: >-
(github.event_name != 'issue_comment' && vars.IRONBEE_VERIFICATION_AUTO != 'false') ||
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) &&
startsWith(github.event.comment.body, '/ironbee-verify'))Or hand it to the action — one line, and it decides.
- uses: ironbee-ai/ironbee-action@v0
with:
verification_auto: ${{ vars.IRONBEE_VERIFICATION_AUTO }} # 'false' switches it offThe action's first real step resolves to "nothing to do", every step after it is skipped, and the job ends green in seconds — no verification, no job charged against your quota, no gate.
What the input cannot do is give back the steps that ran before it. An action
is a step: by the time its first line executes, your checkout, image builds and
dependency installs have already happened, and nothing inside a step can reach
back before itself. In a workflow that builds Docker images that is most of the
run's cost. So: the input is the convenient one, the if: is the one that
actually saves the time. Use the if: when the job does expensive setup, the
input when it does not — or both, since the input is harmless once the if: is
there.
Neither switches off a run somebody asked for by name: a comment command and
a workflow_dispatch still verify. "Automatic" describes a push and a pull
request, not a person pressing a button.
on:
workflow_dispatch:Trigger a full verification manually from the GitHub Actions UI. Creates a fix PR if issues are found.
The action requires these GitHub token permissions:
| Permission | Required | Purpose |
|---|---|---|
contents: write |
Yes | Commit fixes to PR branches, create fix branches |
pull-requests: write |
Yes | Post verification report comments on PRs, create fix PRs |
issues: write |
Yes | Update PR comments, and add reactions on a comment-triggered run |
A pull request from a fork receives no secrets, so neither verification mode can run there. The action detects it and says so rather than failing on an empty key.
The action prints the timeout-minutes its configuration needs, at the top of
the run. Set at least that much on the job:
jobs:
verify:
runs-on: ubuntu-latest
timeout-minutes: 90It cannot check this for you — GitHub exposes a job's own timeout to nothing running inside it. When the step is killed mid-run the verification job is cancelled and the run reads as a platform failure, which is the one failure mode worth spending a line of configuration to avoid.
A preview deployment behind SSO or a protection bypass answers 401 or 403 to the verification, and the run would report on the login page. The action refuses that case in preflight, by name.
An SSO redirect that answers 200 with a login page cannot be told apart from an application by a status code, so it is not caught — the verdict will describe the login page.
Two ways past it:
app_secret_headers— if the protection can be bypassed with a header, put it there. The value is encrypted before it is stored and is never returned by the API. It is readable inside the run itself, which is the caller's own verification of the caller's own deployment; do not use this field for anything that protects more than the target.- Verify on the runner instead — set
app_start_commandandapp_port, and leaveapp_urlempty. This verifies the application rather than the deployment: edge configuration, rewrites and the built artifact are not exercised.
- uses: ironbee-ai/ironbee-action@v1
with:
ironbee_api_key: ${{ secrets.IRONBEE_API_KEY }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
app_build_command: 'npm run build'
app_start_command: 'npm run dev'
app_url: 'http://localhost:3000'- uses: ironbee-ai/ironbee-action@v1
with:
ironbee_api_key: ${{ secrets.IRONBEE_API_KEY }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
verification_prompt: 'Focus on the checkout flow and payment form validation'
claude_code_max_turns: '30'- uses: ironbee-ai/ironbee-action@v1
with:
ironbee_api_key: ${{ secrets.IRONBEE_API_KEY }}
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}The files IronBee generates (.ironbee/, .claude/, .mcp.json, .gitignore) are kept out of the commits this action makes, because they are its working files rather than the change being verified. Turn that off to let a run commit the setup it generated — useful if you want it available for local development:
- uses: ironbee-ai/ironbee-action@v1
with:
ironbee_api_key: ${{ secrets.IRONBEE_API_KEY }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
ironbee_exclude_files: 'false'The PR comment includes a link to the IronBee Console for the verification session. The host is derived from ironbee_collector_url's stage, falling back to console.ironbee.ai. Name it explicitly if you self-host:
- uses: ironbee-ai/ironbee-action@v1
with:
ironbee_api_key: ${{ secrets.IRONBEE_API_KEY }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
ironbee_console_url: 'console.example.com'By default only the browser platform runs. Opt into the others when you want IronBee to verify those layers too. They apply to local mode — a platform run uses the platforms its harness declares:
- uses: ironbee-ai/ironbee-action@v1
with:
ironbee_api_key: ${{ secrets.IRONBEE_API_KEY }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
ironbee_browser_devtools: 'true'
ironbee_backend_devtools: 'true'
ironbee_node_devtools: 'false'
ironbee_python_devtools: 'false'
# A PTY on the runner — enable it only when the CLI is what you are verifying.
ironbee_terminal_devtools: 'false'For any IronBee CLI setting not exposed as a dedicated input, pass a JSON object via ironbee_extra_config. It is deep-merged into the generated .ironbee/config.json and user keys win:
- uses: ironbee-ai/ironbee-action@v1
with:
ironbee_api_key: ${{ secrets.IRONBEE_API_KEY }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
ironbee_extra_config: |
{
"verification": { "enable": true },
"browserDevTools": { "env": { "LOG_FILE": "/tmp/browser-devtools.log" } }
}- uses: ironbee-ai/ironbee-action@v1
with:
ironbee_api_key: ${{ secrets.IRONBEE_API_KEY }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
verbose: 'true'| Input | Required | Default | Description |
|---|---|---|---|
| Where the verification runs | |||
verification_mode |
No | auto |
auto, platform, or local — see Two ways to verify |
| IronBee — account & endpoints | |||
ironbee_api_key |
Yes | IronBee API key. Authenticates the collector, and the platform API in platform mode | |
ironbee_api_url |
No | IronBee API base URL. Empty uses the CLI default | |
ironbee_collector_url |
No | IronBee collector endpoint URL. Empty uses the CLI default — and leaving it empty is what lets the CLI resolve every other service URL to the same stage, since an explicit one here is how it infers which stage that is | |
ironbee_console_url |
No | IronBee Console hostname (no scheme) for session links in the report. Empty derives it from ironbee_collector_url, so a non-production collector does not produce production links |
|
ironbee_cli_version |
No | latest |
IronBee CLI version to install. Empty means latest, so an inputs.x || vars.X chain that resolves to nothing still works |
ironbee_project |
No | Project the results attach to. Empty derives it from the git remote | |
| IronBee — the verification job (platform mode only) | |||
ironbee_bind_repository |
No | true |
Bind the run to the repository so the agent reads the changeset. Turn it off to verify the application without repository context — what a private repository the IronBee GitHub App does not cover needs |
ironbee_job_timeout_minutes |
No | Run timeout for the verification job. Empty uses the API default | |
| IronBee — DevTools platforms (local mode only) | |||
ironbee_browser_devtools |
No | true |
Web UI, DOM, console, a11y, screenshots, recording |
ironbee_backend_devtools |
No | false |
HTTP, gRPC, GraphQL, WebSocket, DB, logs |
ironbee_node_devtools |
No | false |
Node.js runtime — tracepoints, exceptions, variables, HTTP capture |
ironbee_python_devtools |
No | false |
Python runtime (debugpy) — tracepoints, exceptions, logs, thread dumps |
ironbee_terminal_devtools |
No | false |
CLI, REPL and TUI verification by driving a PTY. This gives the agent a shell on the runner — enable it only where that is the thing being verified |
| IronBee — config & repo hygiene | |||
ironbee_extra_config |
No | Raw IronBee config (JSON) deep-merged into .ironbee/config.json; user keys win |
|
ironbee_exclude_files |
No | true |
Keep IronBee-generated files (.ironbee/, .claude/, .mcp.json, .gitignore) out of the commits this action makes. Turn it off to let a run commit the IronBee setup it generated |
| Claude Code — auth | |||
anthropic_api_key |
Yes* | Anthropic API key for Claude Code | |
claude_code_oauth_token |
No* | Claude Code OAuth token (alternative auth) | |
| Claude Code — install & runtime (local mode only) | |||
claude_code_cli_version |
No | latest |
Claude Code CLI version to install. Empty means latest |
claude_code_model |
No | Claude model for the agent that runs on this runner. Platform mode uses the platform default | |
claude_code_max_turns |
No | 100 |
Maximum conversation turns for the agent that runs on this runner |
claude_code_args |
No | Additional Claude Code CLI arguments | |
| What to verify, and what to do about it | |||
verification_prompt |
No | Additional instructions for the verification agent. Reaches whichever engine runs | |
verification_apply_fix |
No | true |
Apply fixes for the issues a failing verification reports. Needs an Anthropic credential; without one the run verifies and reports only |
| Application under test | |||
app_install_command |
No | Command to install dependencies | |
app_build_command |
No | Command to build the application | |
app_start_command |
No | Command to start the application | |
app_url |
No | Application URL for verification. A routable URL is verified as a deployment; a localhost one means the application runs on this runner and is reached through a reverse tunnel |
|
app_port |
No | Local port the application listens on, for a tunnelled target. Required when app_start_command is set without an app_url, or when app_url points at localhost without a port. No default — a guessed port tunnels to nothing. The port in app_url wins when both name one |
|
app_restart_command |
No | Command that restarts the application so it picks up the fixes, run between the fix round and the re-verification. Empty stops the process this action started and re-runs app_start_command — right only when that command runs the application itself. One that hands off (docker compose, a process manager) leaves the pre-fix code running, and the re-verification then reports a confident result about code that no longer exists. For a compose project: docker compose up -d --build |
|
app_logs_command |
No | Command that prints the application's own logs, captured into the evidence artifact. Needed whenever app_start_command hands off — what the action captures then is lifecycle chatter, not the application, so a service that died or stopped consuming leaves no trace. For compose: docker compose logs --no-color --tail=2000 |
|
app_wait_seconds |
No | How long to wait for the local application to accept connections. Empty uses the CLI default (60s) | |
app_headers |
No | Request headers for a deployed URL, one Name: value per line. Non-secret values only — they are stored in the clear and readable by anyone with any of the account's credentials |
|
app_secret_headers |
No | Request headers whose values are secret — a deployment-protection bypass, an environment token. Same Name: value per line, from a GitHub secret. Encrypted before storage and never read back |
|
| GitHub | |||
github_token |
No | github.token |
GitHub token for PR operations |
| Action — general behaviour | |||
working_directory |
No | . |
Working directory for verification |
verbose |
No | false |
Log tool responses, the prompt, the artifact list and verdict details |
Every true/false input also accepts 1/0, yes/no and on/off. Anything else
fails the run naming the input — GitHub does not type-check action inputs, so a value
nobody validates is a setting that silently did not apply.
*Required for local mode, and for fixing in platform mode. A platform run that only verifies needs neither.
| Output | Description |
|---|---|
verdict |
pass, fail, not_applicable, or unknown |
mode |
Where the verification ran: platform or local |
job_id |
Verification job id — platform mode only |
job_url |
IronBee Console link for the run — platform mode only, once the job has started |
artifacts_url |
Download URL for verification evidence — local mode only. In platform mode the evidence is captured on the platform and lives in the Console; this output is empty |
- Setup — Installs
@ironbee-ai/cli,@anthropic-ai/claude-code,@ironbee-ai/devtools, and Playwright Chromium (cached across runs) - Configure — Writes
.ironbee/config.jsonwith the collector URL, the DevTools platform enable flags, and the MCPLOG_FILEpaths under.ironbee/artifacts/(deep-mergingironbee_extra_configon top), then runsironbee install --client claudeto set up hooks, skills, rules, and MCP config. The IronBee API key is passed via theIRONBEE_API_KEYenv var (not written to disk) and inherited by hooks and MCP subprocesses - Verify — Claude Code runs
/ironbee-verify(or/ironbee-verify fullfor manual/scheduled) which:- Reviews the code diff (push/PR) or tests the full app (manual/scheduled)
- Builds and starts the application
- Records sessions via the enabled DevTools modes and takes screenshots
- Tests functionality and checks for errors
- Submits a verdict (pass/fail)
- Fix — If verification fails, Claude Code fixes the issues and re-verifies
- Report — Posts a verification report on the PR with a session-level IronBee Console link, per-cycle verification links (
activityId+verificationId), and a downloadable evidence artifact
- PR trigger — fixes are committed and pushed to the PR branch
- Push / Manual / Scheduled — fixes go to a new branch and a PR is opened
The action performs the commit and the push itself. The agent only edits files.
One fix round per run, and a re-verification only where it can mean something.
A tunnelled target is re-verified: the application is restarted with the fixed code and a second verification runs. A deployed URL is not — the deployment still serves the code that was deployed, so a second run would verify the same bytes and report a result unrelated to the fix. There the loop closes when the fix is deployed and the workflow runs again.
Screenshots and recordings are organized by verification cycle:
.ironbee/artifacts/
cycle-1/
screenshots/homepage-before-fix.png
recordings/verification.webm
cycle-2/
screenshots/homepage-after-fix.png
recordings/verification.webm
Evidence is uploaded as GitHub Actions artifacts (30-day retention) and linked in the PR comment.
Playwright Chromium binaries (~200MB) are cached using actions/cache to speed up subsequent runs. The cache key is based on the runner OS. Browser downloads during npm install are skipped (PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=true) and installed separately with system dependencies.
MIT