How to let an agent create GitHub-signed commits and PRs under a bot identity
(e.g. opc-dashboard-agent[bot]) instead of using a human account or local
unsigned git commit.
Commits are created server-side via GitHub's GraphQL createCommitOnBranch
mutation, so GitHub signs them. The local working tree is then reset to match.
Three scripts in ~/bin (already on PATH, so effectively system-wide):
| Script | Role |
|---|---|
github-app-token |
Builds an RS256 JWT from the App's ID + private key, exchanges it for a short-lived (~9 min) installation access token. |
agent-gh |
Thin gh wrapper: exports GH_TOKEN from github-app-token, then exec gh "$@". Every call gets a fresh token. |
github-agent-commit |
Stages → builds a createCommitOnBranch GraphQL mutation (base64 file additions/deletions) → GitHub creates and signs the commit → resets local branch to origin/<branch>. |
Key constraints baked into github-agent-commit:
- Refuses to commit on
main/master. - Requires staged changes (
git add/git rmfirst). - Supports added/modified/deleted files. Not renames, copies, typechanges, submodules.
- Creates the remote branch from local
HEADif it doesn't exist yet.
Auth model — three env vars drive everything:
GITHUB_APP_IDGITHUB_APP_INSTALLATION_IDGITHUB_APP_PRIVATE_KEY(path to the.pemfile)
Do this once per machine / GitHub account.
Throughout this doc,
opc-dashboard-agentis just the example App name — substitute your own (e.g.my-repo-agent) wherever it appears.
- GitHub → Settings → Developer settings → GitHub Apps → New GitHub App.
- Name it (e.g.
opc-dashboard-agent). Homepage URL can be a placeholder. - Permissions — at minimum:
- Repository → Contents: Read and write
- Repository → Pull requests: Read and write
- Repository → Metadata: Read-only (auto)
- Uncheck "Webhook → Active" (not needed).
- Where can it be installed: "Only on this account".
- Create the App. Note the App ID.
On the App's settings page → Private keys → Generate a private key.
A .pem downloads. Store it somewhere stable and private, e.g.:
~/.config/github-apps/opc-dashboard-agent/private-key.pem
chmod 600 ~/.config/github-apps/opc-dashboard-agent/private-key.pem
Never commit this file. Never print it.
On the App page → Install App → choose your account → select the repos
it should act on. After installing, the URL contains the installation ID:
https://github.com/settings/installations/<INSTALLATION_ID>.
You can also list installations with the helper:
GITHUB_APP_ID=<id> \
GITHUB_APP_PRIVATE_KEY=~/.config/github-apps/opc-dashboard-agent/private-key.pem \
github-app-token --installation-idThis prints id <tab> account <tab> repository_selection.
If ~/bin/github-app-token, ~/bin/agent-gh, ~/bin/github-agent-commit
don't already exist, create them (see Appendix — Script sources below) and:
chmod +x ~/bin/github-app-token ~/bin/agent-gh ~/bin/github-agent-commitEnsure ~/bin is on PATH. Dependencies: gh, jq, git, base64,
openssl, curl.
Add to your shell profile (~/.zshrc) — or better, a sourced secrets file:
export GITHUB_APP_ID="123456"
export GITHUB_APP_INSTALLATION_ID="78901234"
export GITHUB_APP_PRIVATE_KEY="$HOME/.config/github-apps/opc-dashboard-agent/private-key.pem"echo "$(github-app-token)" | cut -c1-12 # should print a token prefix like ghs_xxxxxxxx
agent-gh auth statusThe scripts and machine env are already done. For each new repo:
GitHub → App settings → Install App → edit repo access → add the new repo. (If the App is org-wide with "All repositories", nothing to do.)
If it's a different account/org, you'll get a new installation ID — update
GITHUB_APP_INSTALLATION_ID accordingly (or switch per-repo via a direnv
.envrc).
Repo → Settings → Branches → Add branch protection rule for main:
- Require a pull request before merging.
- (Optional) Require status checks. This enforces the "no direct commits to main" rule.
Paste the following block into the repo's CLAUDE.md, replacing
<repo>-agent[bot] with this repo's App identity. These rules tell the agent
to use github-agent-commit / agent-gh and forbid the unsigned-local-commit
path.
## Project GitHub Agent Rules
This repository is configured for GitHub App based agent work.
The agent identity is:
<repo>-agent[bot]
The default branch is protected. Direct commits and direct pushes to `main` are forbidden.
All agent-created mergeable commits must be created through GitHub's API so GitHub signs the commit as the GitHub App.
---
## Required Workflow
Always work on a feature branch.
git switch -c agent-short-description
Make changes locally, then inspect them:
git status --short
git diff
Stage the intended changes:
git add path/to/file
For deletions:
git rm path/to/file
Before creating a commit, show the staged summary:
git status --short
git diff --cached --stat
Create commits only with:
github-agent-commit "Descriptive commit message"
Open pull requests with:
agent-gh pr create --base main --head "$(git branch --show-current)" --title "PR title" --body "PR body"
Use `agent-gh` instead of plain `gh` for GitHub CLI operations so a fresh GitHub App token is minted automatically.
---
## Forbidden Commands
Never run:
git commit
git commit -m
git commit -S
git commit --amend
git commit-tree
git merge --commit
git cherry-pick
git rebase
git am
git push origin main
gh pr merge --admin
Do not create local Git commits. Local commits will not be signed by GitHub as the app and may fail repository signature requirements.
Do not push directly to `main`.
Do not use the human account credentials or personal access tokens.
Do not print secrets, tokens, private keys, or environment dumps.
Avoid commands such as:
echo "$GH_TOKEN"
echo "$GITHUB_TOKEN"
env
printenv
cat ~/.config/github-apps/<repo>-agent/private-key.pem
Safe token checks may show only a short prefix:
echo "$GH_TOKEN" | cut -c1-12
---
## Allowed Git Commands
These commands are allowed when needed:
git status
git status --short
git diff
git diff --cached
git diff --cached --stat
git add
git rm
git restore
git restore --staged
git switch
git branch
git fetch
git pull --ff-only
git log
git show
git reset --hard origin/branch-name
Use caution with destructive commands. Do not discard user work unless explicitly instructed.
---
## GitHub CLI Rules
Prefer:
agent-gh
instead of:
gh
Examples:
agent-gh auth status
agent-gh pr create --base main --head "$(git branch --show-current)"
agent-gh pr view
agent-gh api /repos/<owner>/<repo>
Reason: `agent-gh` mints a fresh short-lived GitHub App installation token for each command.
---
## Commit Rules
Use:
github-agent-commit "Message"
This command reads staged changes and creates a GitHub API commit on the current branch.
Expected result:
signature state: VALID
signature valid: true
signed by GitHub: true
If `github-agent-commit` fails, do not fall back to `git commit`. Report the error and stop.
---
## Branch Rules
Never work directly on `main`.
Before editing, check the current branch:
git branch --show-current
If on `main`, create a feature branch:
git switch -c agent-short-description
Branch names should be short and descriptive, for example:
agent-fix-login-state
agent-add-dashboard-filter
agent-update-readme
---
## Pull Request Rules
Open PRs against `main`.
Use:
agent-gh pr create \
--base main \
--head "$(git branch --show-current)" \
--title "Clear title" \
--body "Summary of changes and testing performed."
---
## Testing Expectations
Before creating a commit, run relevant checks when available.
At minimum, inspect the diff:
git diff --cached
If the project has known test/lint commands, prefer those before committing.
Do not invent successful test results. If a test was not run, say so.
---
## Token and Credential Model
The shell uses GitHub App credentials:
GITHUB_APP_ID
GITHUB_APP_INSTALLATION_ID
GITHUB_APP_PRIVATE_KEY
Short-lived GitHub tokens are minted on demand.
Do not try to fix authentication by switching to the human account.
Do not run:
gh auth login
gh auth refresh
gh auth switch
If GitHub authentication fails, prefer:
agent-gh auth status
and report the error.
---
## Summary
The correct agent workflow is:
git switch -c agent-branch-name
# edit files
git status --short
git diff
git add path/to/files
git diff --cached --stat
github-agent-commit "Commit message"
agent-gh pr create --base main --head "$(git branch --show-current)" --title "Title" --body "Body"
The most important rule:
Never use git commit. Use github-agent-commit.cd /path/to/new-repo
git switch -c agent-test-signing
echo "test" > .agent-signing-test
git add .agent-signing-test
github-agent-commit "Test: verify GitHub App signed commits"Expected output includes:
signature state: VALID
signature valid: true
signed by GitHub: true
Then clean up the test branch.
git switch -c agent-short-description
# edit files
git status --short
git diff
git add path/to/file # git rm path/to/file for deletions
git diff --cached --stat
github-agent-commit "Descriptive commit message"
agent-gh pr create --base main --head "$(git branch --show-current)" \
--title "PR title" --body "PR body"Never use git commit — it produces an unsigned local commit that won't carry
the App signature and may be rejected by branch protection.
| Symptom | Cause / fix |
|---|---|
GITHUB_APP_ID is required |
Env vars not exported in this shell. |
could not fetch repository id |
App not installed on this repo, or wrong installation ID. |
refusing to create an agent commit directly on main |
Switch to a feature branch first. |
no staged changes found |
Run git add before github-agent-commit. |
renames/copies/typechanges are not supported |
Stage the change as separate delete + add. |
remote branch ... does not exist, and local HEAD is not known to GitHub |
Push the parent commit first, or branch from an already-pushed commit. |
Token works but gh fails |
Use agent-gh, not plain gh — plain gh uses your human credentials. |
The three scripts currently installed at ~/bin/. Reproduce them on a new
machine.
Builds an RS256 JWT (iat = now-60s, exp = now+540s) signed with the App
private key via openssl dgst -sha256 -sign, then POSTs to
/app/installations/<id>/access_tokens to get the installation token.
With --installation-id it instead lists all installations.
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export GH_TOKEN="$("$SCRIPT_DIR/github-app-token")"
export GITHUB_TOKEN="$GH_TOKEN"
exec gh "$@"Parses origin URL → owner/repo, fetches the repo node ID and remote branch
HEAD, base64-encodes each staged file into a GraphQL additions/deletions
payload, runs the createCommitOnBranch mutation, prints the signature status,
then git fetch + git reset --hard origin/<branch> to sync the local tree.
Resolves its sibling github-app-token the same way agent-gh does
($(dirname "${BASH_SOURCE[0]}")), so the three scripts are portable as a set
— drop them in any directory on PATH and they work without edits.
The canonical, full-length copies live at
~/bin/. Reproduce verbatim on a new machine; the scripts resolve each other relatively, so no path edits are needed.