From Issue to Merge: A GitHub-Native Coding Agent Powered by Amazon Bedrock
Run Claude Code as an autonomous coding agent in GitHub Actions, with inference on Amazon Bedrock. Assign an issue, get a tested pull request. No Anthropic account, no keys, and no laptop required.
Most of us have worked with an AI coding assistant embedded in an IDE or terminal. It's excellent for in-the-moment pairing, but its usefulness and scalability ends where your desk does—close the laptop and the work stops. The real opportunity is agents that keep going without us: a fleet running in the cloud, operating directly against our GitHub repositories on infrastructure we already trust. An assistant tethered to a single machine can never deliver that.
This post walks through a pattern that does exactly that: Claude Code running inside GitHub Actions, with all model inference served through Amazon Bedrock. You assign an issue (or
@ -mention the agent), it implements the change on a feature branch, runs your tests, and opens a PR. Comment on that PR and it iterates in place. No separate Anthropic account, no keys on a workstation, no agent process running under your desk.A complete, working reference (the workflow file plus a documented README) is here:
Everything below is generalized from that repo. Copy the workflow file into your repo, drop in your own values, and you have a cloud-hosted coding agent ready to go.
Why route through Amazon Bedrock?
If you're an AWS shop, serving the model through Bedrock instead of a direct provider API changes the operational story in a few meaningful ways.
No separate Anthropic account or API key to manage. The agent authenticates to Bedrock with a short-lived IAM role assumed via GitHub OIDC. There's no long-lived provider key sitting in a secret store, no second vendor relationship, no second invoice. Model access is just another IAM permission (
bedrock:InvokeModel ).Your data governance already extends to it. With Bedrock, inference requests are made with your IAM credentials, stay in your chosen region, appear in your CloudTrail, and are never used to train models. Route them through PrivateLink and traffic never touches the public internet. Pair that with self-hosted runners in your VPC and the entire loop runs under controls you already audit. For compliance teams, "it's a Bedrock call in our own region" is a far easier conversation than "we send code to a third-party API."
Consolidated billing and cost controls. Model spend shows up on your AWS bill alongside everything else, and you can govern it with the same budgets, cost allocation tags, and alarms.
Model choice is a first-class knob. Bedrock hosts the full Claude family, and as you'll see, this workflow lets you pick the exact model per task.
The core idea: issues in, pull requests out
The workflow turns your issue tracker into a work queue for the agent. The loop is deliberately simple:
- You write an issue describing what you want.
- You assign it to the agent's GitHub user (or comment
@your-agent implement this).
- The agent creates a feature branch, makes the changes, runs the test suite, and opens a PR that references the issue.
- You review. If something's off, you comment on the PR, and the agent updates the same branch.
- When it's green and you're happy, you merge.
That fourth step is worth pausing on, because it's where a lot of naive "AI opens a PR" setups fall down.
It responds to PR comments, even after it's "done"
A common frustration with first-generation setups is that the agent opens a PR and then can't touch it again. Comment "please also handle the null case" and you get a brand-new PR, or nothing at all. This workflow wires up both PR comment surfaces:
- Comments on the PR conversation tab arrive as
issue_commentevents (in GitHub's data model, a PR is an issue).
- Comments left inline on the diff arrive as
pull_request_review_commentevents.
Both are handled, and when the trigger is a comment on an existing PR, the workflow runs
gh pr checkout so the agent lands on that PR's feature branch and pushes commits back to it. The result: you can have a genuine back-and-forth on a PR ("rename this", "add a test for the error path", "use the existing helper instead") and the agent revises its own work in place. It's code review as a conversation, not a one-shot.
Pick the model and turn budget per task, with labels
You choose which Claude model runs and how many turns it may take just by applying labels to the issue or PR before you trigger it. Labels are read live at run time, so you can add one right before assigning.
Model selection uses a
model:<name> label:Label | Model | Rough use |
|---|---|---|
model:haiku (or model:cheap) | Haiku, cheapest | Docs, renames, config, one-file fixes |
model:fable5 (or model:fable) | Fable 5 | Lightweight tasks; verify Bedrock availability in your region first |
model:sonnet | Sonnet, balanced (default) | Most feature and bugfix work |
model:opus (or model:max) | Opus, most capable | Hard, multi-file, architectural changes |
Turn budget uses a
max-turns:<n> label, for example max-turns:100 , to cap how many agent turns a run may take. No label means a sensible default (60 in the reference repo).A few examples of how that plays out:
- Fix a typo in the README. Add
model:haiku, comment@your-agent fix the typo in the intro. You just spent pennies.
- Refactor a module across several files. Add
model:opusandmax-turns:150, then assign the issue. You've given the strong model room to work.
- Ordinary bugfix. Add nothing. It runs on the balanced default.
Because the label is read at runtime (not baked into the workflow), non-developers on the team can drive cost and quality trade-offs without ever touching YAML.
One important note: Claude Code only runs on Claude models, so these tiers are the Claude family. And you may need to enable a model in your Bedrock account and region and confirm its inference-profile ID. Some are dated (us.anthropic.claude-haiku-4-5-20251001-v1:0), some are suffix-less (us.anthropic.claude-opus-4-8). Fable 5 in particular: confirm it's offered in your region before you rely on themodel:fable5label.
Setup, start to finish
Here's the general setup:
1. Create a GitHub App to be the agent's identity
The workflow authenticates as a dedicated GitHub App, not the default
GITHUB_TOKEN . This matters: the default token is read-only unless you widen it, and it cannot push to a PR from a fork. An App installation token can do both, and it gives the agent a consistent identity ( your-agent[bot] ) so its commits and comments are attributable.Give the App these repository permissions and install it on your repo:
- Contents: Read & write, to push commits to branches
- Pull requests: Read & write, to open PRs, comment, and update
- Issues: Read & write, to comment on issues and read labels
Generate a private key and note the App ID and Client ID. GitHub makes this pretty easy to do in the web console.
2. Wire up Bedrock access with OIDC (no static keys)
Create an IAM role that GitHub Actions can assume via OIDC. Its trust policy allows your repo's OIDC subject; its permission policy grants
bedrock:InvokeModel and bedrock:InvokeModelWithResponseStream for the models you plan to use. Then enable those Claude models in Bedrock in your chosen region.Using OIDC means there are no long-lived AWS credentials stored in GitHub. The runner exchanges a short-lived token for temporary role credentials on each run.
3. Add secrets and one variable
Repository secrets (Settings, then Secrets and variables, then Actions):
Secret | Purpose |
|---|---|
AGENT_APP_ID | GitHub App ID, this is deprecated but worthwhile to keep track of |
AGENT_CLIENT_ID | GitHub App client ID |
AGENT_APP_PRIVATE_KEY | GitHub App private key (PEM) |
AGENT_OIDC_ROLE_ARN | ARN of the IAM role to assume for Bedrock |
Repository variable (Settings, then Secrets and variables, then Variables):
Variable | Purpose |
|---|---|
GITHUB_AGENT_USER | The agent's GitHub username / App slug |
GITHUB_AGENT_USER is the single place that defines the agent's identity. The trigger conditions read it as vars.GITHUB_AGENT_USER , so you can swap the agent user without editing the workflow. It has to be an Actions variable, not an env , because a job-level if: can read vars but not env .4. Commit the workflow
Drop the workflow into
.github/workflows/ on your default branch, and you're live. Assign an issue to the agent to take it for a spin.
Set the agent up to succeed: write a CLAUDE.md first
If you take one thing from this post, take this: create a
CLAUDE.md in your repo before you point the agent at real work. It's the single highest-leverage thing you can do for output quality.CLAUDE.md is a file Claude Code reads automatically as standing context for every run. Think of it as the onboarding doc you'd give a new engineer, except the agent actually reads all of it, every time. Without it, the agent infers your conventions from the code and will sometimes guess wrong: the wrong test runner, a deprecated pattern, a module boundary it shouldn't cross. With it, you're steering every implementation toward your idea of correct.Good things to put in
CLAUDE.md :- How to build, test, and lint. The exact commands. "Run
npm testandnpm run lint; both must pass before opening a PR." This is what lets the agent self-verify.
- Architecture and boundaries. Where things live, what depends on what, what not to touch. "Business logic goes in
src/domain; never import AWS SDK clients directly in domain code."
- Conventions. Naming, error handling, logging, commit message style, how you write tests.
- Guardrails. "Don't add new dependencies without flagging it." "Don't modify generated files under
gen/." "Prefer editing existing modules over creating new ones."
- Domain context. Acronyms, the meaning of key entities, links to internal patterns.
There's a nice cost side effect, too. Prompt caching (on by default in Claude Code) discounts repeated context heavily, so a stable
CLAUDE.md is largely "paid for once" across runs. A tight, accurate CLAUDE.md reduces the number of turns the agent burns exploring your codebase, which directly lowers your bill and produces PRs that need fewer review cycles.
Best practices: cheaper runs, better PRs
A few habits make this pattern genuinely productive rather than a novelty.
Write issues like specs, not tweets. The agent is only as good as the request. "Fix the bug" burns turns while it hunts. Compare that to: "In
src/auth/token.ts , refreshToken() doesn't handle a 401 from the refresh endpoint; it should retry once then surface AuthError . Add a unit test for the 401 path." That gets you a clean PR on the first pass. Consider a GitHub issue template that prompts for acceptance criteria, affected files, and how to test.Match the model to the job. This is the biggest cost lever, roughly a 5x spread between the cheapest and most capable model. Default to the balanced tier, reach for the top tier only when the task is genuinely hard, and drop to the cheap tier for mechanical edits. The label system makes this a per-issue decision.
Cap turns to fit the task.
max-turns bounds worst-case spend on a single run. Keep the default modest and raise it only for big changes. A runaway agent on a trivial task should hit a low ceiling, not your budget.Insist on green tests before merge. The reference workflow's prompt tells the agent to run tests and only open a PR when they pass, and to explain itself (and not open a PR) when it's blocked, when requirements are unclear, or when it can only do part of the job. Encode your quality bar the same way: the agent should treat "tests pass" as non-negotiable, and your branch protection rules should back that up with required checks.
Keep humans on the merge button. The agent proposes; you dispose. Require review and passing CI on protected branches. The agent's PRs go through the exact same gate as a human's.
Mind the permissions blast radius. The reference workflow runs with broad tool access because it lives on an ephemeral GitHub-hosted runner. The container is thrown away after each run, so the blast radius is contained. If you move this to a persistent or self-hosted runner, tighten the allowed tools and don't run as root. Treat the runner like production infrastructure, because it can now write to your repo.
Watch for self-triggering loops. Because the agent comments on issues and PRs, and comments can trigger the workflow, you need a guard so its own comments don't wake it up again. The reference workflow excludes comments authored by
your-agent[bot] in the job condition, a small detail that saves you from an infinite loop (and a surprising bill).
Security and guardrails
The best-practices above keep the agent productive. This section is about keeping it safe. Two controls matter most, and both come down to the same principle: grant only what Claude Code actually needs to do the job, and nothing more.
Scope the agent's tools to exactly what it needs
To get started quickly, the reference workflow runs Claude Code with
--dangerously-skip-permissions , which lets it invoke any tool without prompting. That's a reasonable starting posture on an ephemeral GitHub-hosted runner, where the container is destroyed after each run, but it is not where you want to stay. A skip-all-permissions agent can run arbitrary shell, which on a runner that holds an AWS role and a GitHub App token is more capability than a coding task requires.Once you've watched a few runs and know what your workflow genuinely uses, replace the skip flag with an explicit allow-list. Claude Code accepts
--allowedTools , and you pass exactly the tools you need:claude_args: | --model ${{ steps.pick-model.outputs.model }} --max-turns ${{ steps.pick-model.outputs.max_turns }} --allowedTools "Edit,Read,Write,Bash(git:*),Bash(npm test),Bash(npm run lint)"
The idea is to enumerate the file operations the agent needs (
Edit , Read , Write ) and to scope shell access to specific commands rather than a blanket Bash . Prefer narrow patterns like Bash(npm test) over Bash(npm:*) , and only add Bash(git:*) because committing and pushing is core to the workflow. Anything not on the list is denied by default. If you find the agent legitimately needs another command, add it deliberately rather than reopening the floodgates. This turns "the agent can do anything" into "the agent can edit files, run the tests, and push a branch," which is all the job actually requires.Keep in mind that the tool allow-list and the credentials the runner holds are two different layers. Even a well-scoped tool list runs alongside a GitHub App token and an assumed AWS role, so tighten those too, which brings us to the next control.
Scope the OIDC role to least privilege
The IAM role the runner assumes is the agent's key to your AWS account. Treat it like any other production principal and scope it on two axes: who can assume it, and what it can do.
On the "who" side, constrain the trust relationship to this specific repository, and ideally to a single branch or a named GitHub Environment, rather than leaving it open to your whole organization. GitHub OIDC supports matching the incoming token's subject claim precisely, so the role can only be assumed from the workflow you intend.
On the "what" side, grant only the two Bedrock invoke actions the agent uses, scoped to the specific models and region you've enabled. The role does not need broad Bedrock access, and it certainly does not need a path to the rest of your account. Resist the temptation to reuse a broad deploy role you already had lying around; create a dedicated role for this workflow so its blast radius is obvious and small.
A few habits reinforce this: set a recognizable session name so the assumed-role sessions are easy to spot in CloudTrail, review CloudTrail periodically to confirm the role only ever invokes Bedrock and only from your Actions runs, and if the agent ever needs another AWS service (say, to run integration tests), grant that narrowly and against a dedicated non-production account rather than widening this role.
Require branch protection and keep merges in human hands
The tool allow-list and the OIDC role control what the agent can do. Branch protection controls what actually lands in your codebase, and it's the backstop that lets you run an autonomous agent with confidence.
The agent's job is to propose changes, never to ship them. Configure branch protection on your default branch (and any other protected branches) so that:
- Pull requests are required. No direct pushes to protected branches, from the agent or anyone else. The agent already works on feature branches and opens PRs, so this simply enforces the pattern.
- A human review and approval is required before merge. Require at least one approving review from a person, and make sure the agent cannot approve or merge its own PRs. An App or bot identity should be able to open and update a PR, but the merge button belongs to a human. Review your ruleset and any auto-merge settings to confirm no automation can satisfy the approval or merge on the agent's behalf.
- Status checks must pass. Require your CI (tests, linting, security scans) to be green before merge. This pairs with the agent's own instruction to only open a PR when tests pass, giving you defense in depth: the agent self-checks, and the platform verifies independently.
- Restrict who can merge. Limit merge rights to the specific people or teams responsible for the repo, so that shipping code is always a deliberate human decision.
The result is a clean division of labor: the agent can read the repo, edit files, run tests, and push a branch, but a person is always the one who reads the diff, approves it, and clicks merge. Between a scoped tool allow-list, a least-privilege OIDC role, and branch protection with human-only merge, a compromised prompt or a runaway run is contained. The worst case is an unwanted pull request that never merges, not unwanted code in production.
Wrapping up
The combination here is more than the sum of its parts. Claude Code gives you a capable coding agent; GitHub Actions gives you an event-driven, ephemeral place to run it; Amazon Bedrock keeps the model inference inside your AWS account with your governance and your billing. Bolt on label-driven model and turn selection plus PR-comment iteration, and you get a coding agent that fits how AWS teams already work: issues as the queue, PRs as the interface, IAM as the trust boundary, and humans on the merge button.
Start with a solid
CLAUDE.md , write issues like specs, and let the cheap models handle the boring work. Then, genuinely, close the laptop.The full reference implementation, including the workflow file and a detailed README, is at github.com/grolston/claude-code-github-action-workflow . Copy the workflow file, wire in your values, create a new issue, and watch for your first agent-authored PR.
Model prices and Bedrock model availability change over time and vary by region. Check the Amazon Bedrock pricing page and your account's enabled models before relying on specific figures.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article