AWS Builder Center

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:
  1. You write an issue describing what you want.
  1. You assign it to the agent's GitHub user (or comment  @your-agent implement this ).
  1. The agent creates a feature branch, makes the changes, runs the test suite, and opens a PR that references the issue.
  1. You review. If something's off, you comment on the PR, and the agent updates the same branch.
  1. 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_comment  events (in GitHub's data model, a PR is an issue).
  • Comments left inline on the diff arrive as  pull_request_review_comment  events.
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:opus  and  max-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 the  model:fable5  label.



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 test  and  npm 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.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article