A single idempotent bash script that turns a fresh Linux host into a ready-to-use Claude Code agent environment. Built for Brev VMs but works on any Ubuntu/Debian host.
-
Claude Code — installed via the official native installer, then configured for unattended use:
bypassPermissionsdefault mode,skipDangerousModePermissionPrompt, sandboxed- Model selected via
AAB_CLAUDE_CODE_MODEL(defaults toclaude-opus-4-7), max effort - Inference provider selectable at runtime — either Anthropic's first-party API or any Anthropic-compatible third-party gateway. Switch with
claude_code_switch_inference_provider anthropic|third-party. - Onboarding wizard skipped (no theme / color-scheme prompt on first launch)
ANTHROPIC_API_KEYpre-approved if provided (no first-run approval prompt)claudealiased toclaude --dangerously-skip-permissionsin interactive shells
-
ghCLI — latest release from the officialcli.github.xm233.cnapt repo (the distro-shippedghpredatesgh auth token/gh auth git-credential). -
git —
user.name/user.emailset from env, andghregistered as thegithub.xm233.cncredential helper sogit clone/git pushreuse the gh-stored token with no interactive prompt. IfGIT_SIGNING_PRIVATE_KEY_B64is set, git is also configured to sign every commit and tag with that key (see SSH keys). -
SSH keys for GitHub — two independent optional env vars, each for a distinct role:
GH_AUTH_SSH_PRIVATE_KEY_B64→ the authentication identity. Decoded to~/.ssh/id_aab_auth(mode 0600) and wired as theIdentityFileforgithub.xm233.cnin a managed block in~/.ssh/config.GIT_SIGNING_PRIVATE_KEY_B64→ the signing key. Decoded to~/.ssh/id_aab_signing(mode 0600) and wired into git'suser.signingkey/commit.gpgsign/tag.gpgsignconfig. Does not touch~/.ssh/config.
See SSH keys for how to generate, encode, and upload them.
-
Claude Code plugins — marketplaces listed in
claude_code_plugins.txtare registered in~/.claude/settings.json'sextraKnownMarketplaces, and the plugins they declare are flipped on inenabledPlugins. Claude Code fetches them on next launch, no prompt. Defaults ship agitentic and autocuda (private); add more by editing the file and re-running the bootstrap. Plugin repos can be public or private — the bootstrap fetches each marketplace manifest viagh apiwhenghis authenticated (picks upGH_TOKENorgh auth logincredentials) and falls back to unauthenticatedgithubraw.xm233.cnotherwise. Entries the caller lacks access to are logged and skipped; they do not fail the bootstrap.
To run the bootstrap:
- Ubuntu/Debian host with
bashandapt-get - A bare
ubuntu:22.04container image is a valid starting point — everything else (curl,python3,git,sudo,ca-certificates, andgh) is installed by the script itself on first run - Passwordless
sudo(or running as root) — required so the script can install those packages; it warns and skips otherwise
To run the tests (see Running the tests):
bashshellcheck— for lintbats(≥1.2) andpython3— for the unit suitegitleaks(pinned to v8.18.4 in CI) — for the secret scandocker— for the bare-container end-to-end check- The on-host
--e2ejob doesn't need anything beyondbash; the bootstrap it invokes installs its own prerequisites
From a Brev VM or any Linux host, set your config and paste one of the following install recipes. You can either pass settings via env vars (recipes 1–3) or via a config file (recipe 4) — both accept the same keys.
Use this if you have both a regular Anthropic API key and a third-party Anthropic-compatible gateway, and want to be able to flip between them with claude_code_switch_inference_provider.
export AAB_CLAUDE_CODE_INFERENCE_PROVIDER="anthropic"
export AAB_CLAUDE_CODE_MODEL="claude-opus-4-7"
export AAB_CLAUDE_CODE_MODEL_THIRD_PARTY_PREFIX="aws/anthropic/bedrock-"
export ANTHROPIC_API_KEY="..."
export ANTHROPIC_BASE_URL="..."
export ANTHROPIC_AUTH_TOKEN="..."
export GH_TOKEN="..."
export GIT_AUTHOR_NAME="Your Name"
export GIT_AUTHOR_EMAIL="youremail@gmail.com"
curl -fsSL https://github.com/brycelelbach/autonomous-agent-bootstrap/main/bootstrap.bash | bash
source ~/.bashrc
claude -p "Say hello from Claude Code"export AAB_CLAUDE_CODE_INFERENCE_PROVIDER="anthropic"
export AAB_CLAUDE_CODE_MODEL="claude-opus-4-7"
export ANTHROPIC_API_KEY="..."
export GH_TOKEN="..."
export GIT_AUTHOR_NAME="Your Name"
export GIT_AUTHOR_EMAIL="youremail@gmail.com"
curl -fsSL https://github.com/brycelelbach/autonomous-agent-bootstrap/main/bootstrap.bash | bash
source ~/.bashrc
claude -p "Say hello from Claude Code"export AAB_CLAUDE_CODE_INFERENCE_PROVIDER="third-party"
export AAB_CLAUDE_CODE_MODEL="claude-opus-4-7"
export AAB_CLAUDE_CODE_MODEL_THIRD_PARTY_PREFIX="aws/anthropic/bedrock-"
export ANTHROPIC_BASE_URL="..."
export ANTHROPIC_AUTH_TOKEN="..."
export GH_TOKEN="..."
export GIT_AUTHOR_NAME="Your Name"
export GIT_AUTHOR_EMAIL="youremail@gmail.com"
curl -fsSL https://github.com/brycelelbach/autonomous-agent-bootstrap/main/bootstrap.bash | bash
source ~/.bashrc
claude -p "Say hello from Claude Code"If you didn't pass GH_TOKEN, sign in to gh (gh auth login) before using GitHub.
To wire in GitHub SSH keys, export GH_AUTH_SSH_PRIVATE_KEY_B64 (auth identity for git-over-SSH) and/or GIT_SIGNING_PRIVATE_KEY_B64 (commit & tag signing) before running the bootstrap. See SSH keys for details.
Instead of long export chains, drop the same KEY=VALUE pairs in a file and pass its path as a positional arg:
cat > /tmp/aab.conf <<'CONF'
AAB_CLAUDE_CODE_INFERENCE_PROVIDER=anthropic
AAB_CLAUDE_CODE_MODEL=claude-opus-4-7
ANTHROPIC_API_KEY=...
GH_TOKEN=...
GIT_AUTHOR_NAME=Your Name
GIT_AUTHOR_EMAIL=you@example.com
CONF
# From a local checkout:
bash bootstrap.bash /tmp/aab.conf
# Or from curl-pipe-bash — note the '-s --' that hands the positional
# arg through to the piped script:
curl -fsSL https://github.com/brycelelbach/autonomous-agent-bootstrap/main/bootstrap.bash | bash -s -- /tmp/aab.confSupported line shapes:
KEY=value,KEY="value with spaces",KEY='single quoted'- optional leading
export(for files that double as a sourceable shell snippet); any amount of whitespace betweenexportand the key is tolerated #at the start of a line is a comment; blank lines are ignored- malformed lines (no
=, or a key that isn't a valid shell identifier) emit a warning and are skipped
Values are literal — no shell expansion. FOO=$BAR sets FOO to the literal string $BAR, not the value of $BAR in your shell. This is deliberate: the parser is a KEY=VALUE reader, not a source, so a fat-fingered or malicious config file can't run arbitrary shell. If you want a templated config, expand in your shell first (MY_GATEWAY_BASE=https://… envsubst < template > final.conf) and pass the expanded file to the bootstrap.
Env beats file. If a variable is already set in the shell when you invoke the bootstrap, that value wins over the file entry. This makes one-off overrides easy to test without editing the file:
AAB_CLAUDE_CODE_MODEL=claude-haiku-4-5 bash bootstrap.bash /tmp/aab.confA corollary: there's no way to unset a variable from the file — if FOO is already exported in your shell, the file cannot force it to "unset" (only the env→file direction is valid). FOO= bash bootstrap.bash aab.conf lets you explicitly set FOO to empty, which most of the bootstrap's optional keys treat as "unset".
A missing / unreadable config-file path causes the bootstrap to exit non-zero before touching anything.
The bootstrap writes a claude_code_switch_inference_provider shell function into ~/.bashrc. Call it with anthropic or third-party to flip the active provider — it rewrites the AAB_CLAUDE_CODE_INFERENCE_PROVIDER value in your ~/.bashrc and re-sources it:
claude_code_switch_inference_provider third-partyThe if/else in the managed block unsets the other provider's variables, so you won't get cross-provider env pollution.
All optional. Anything unset is simply skipped.
| Variable | Effect |
|---|---|
AAB_CLAUDE_CODE_INFERENCE_PROVIDER |
anthropic (default) or third-party. Selects which branch of the if/else in the managed ~/.bashrc block is active at runtime. Can be flipped later via claude_code_switch_inference_provider. |
AAB_CLAUDE_CODE_MODEL |
Unprefixed model name (e.g. claude-opus-4-7). Baked into ~/.claude/settings.json's "model" field and exported as ANTHROPIC_MODEL in the anthropic branch. Defaults to claude-opus-4-7. |
AAB_CLAUDE_CODE_MODEL_THIRD_PARTY_PREFIX |
Prepended to AAB_CLAUDE_CODE_MODEL when exporting ANTHROPIC_MODEL in the third-party branch (e.g. aws/anthropic/bedrock- + claude-opus-4-7 → aws/anthropic/bedrock-claude-opus-4-7). |
ANTHROPIC_API_KEY |
Last 20 characters written to ~/.claude.json under customApiKeyResponses.approved so Claude Code doesn't prompt for approval. Also exported from the anthropic branch of the ~/.bashrc managed block. |
ANTHROPIC_BASE_URL |
Exported from the third-party branch. |
ANTHROPIC_AUTH_TOKEN |
Exported from the third-party branch. The third-party branch also exports CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 so context-management beta headers aren't sent to gateways that reject them. |
GH_TOKEN |
Exported from the ~/.bashrc managed block. gh reads it from the environment directly, and since gh auth git-credential is registered as the github.com credential helper, git clone / git push reuse it automatically. |
GIT_AUTHOR_NAME |
git config --global user.name |
GIT_AUTHOR_EMAIL |
git config --global user.email |
GH_AUTH_SSH_PRIVATE_KEY_B64 |
Base64-encoded OpenSSH private key used as the github.com authentication identity. Decoded to ~/.ssh/id_aab_auth (mode 0600); public half at ~/.ssh/id_aab_auth.pub. A managed block in ~/.ssh/config wires it as IdentityFile for github.com with IdentitiesOnly yes. Does not touch git signing config. See SSH keys. |
GIT_SIGNING_PRIVATE_KEY_B64 |
Base64-encoded OpenSSH private key used only as the git commit/tag signing key. Decoded to ~/.ssh/id_aab_signing (mode 0600); public half at ~/.ssh/id_aab_signing.pub. Sets gpg.format=ssh, user.signingkey=~/.ssh/id_aab_signing.pub, commit.gpgsign=true, tag.gpgsign=true. Does not touch ~/.ssh/config. See SSH keys. |
AAB_CLAUDE_CODE_PLUGINS_FILE |
Path to a local claude_code_plugins.txt. If set and the file exists, it's used instead of fetching the canonical list. |
AAB_CLAUDE_CODE_PLUGINS_URL |
URL of the plugin list to fetch when AAB_CLAUDE_CODE_PLUGINS_FILE is unset. Defaults to claude_code_plugins.txt on main of this repo. |
Plugins are listed, one per line, in claude_code_plugins.txt as GitHub owner/repo pointers to Claude Code plugin marketplaces (repos that contain .claude-plugin/marketplace.json). Entries can be public or private repos; private repos are fetched via gh api and require gh to be authenticated (any of GH_TOKEN, GITHUB_TOKEN, or a stored gh auth login credential with access to the repo). For each entry, the bootstrap fetches the marketplace manifest, reads the marketplace name and plugin names it declares, and merges:
extraKnownMarketplaces["<marketplace-name>"] = { "source": { "source": "github", "repo": "<owner/repo>" } }enabledPlugins["<plugin>@<marketplace>"] = true
…into ~/.claude/settings.json. Claude Code fetches and caches the plugins on next launch, at user scope, with no interactive prompt.
To add a plugin: append its marketplace's owner/repo to claude_code_plugins.txt and re-run the bootstrap. To install from your own fork or a different list, set AAB_CLAUDE_CODE_PLUGINS_FILE=/path/to/your.txt or AAB_CLAUDE_CODE_PLUGINS_URL=https://....
If the bootstrap can't fetch a marketplace manifest — usually because the repo is private and the active GitHub credential doesn't grant access — it logs the skip and moves on. Plugin install is treated as optional; an inaccessible entry does not fail the bootstrap.
The bootstrap handles two independent optional env vars for GitHub SSH keys, each governing a distinct role. They can be set together, individually, or not at all.
| Env var | Role | Writes private key to | Touches ~/.ssh/config? |
Touches git signing config? |
|---|---|---|---|---|
GH_AUTH_SSH_PRIVATE_KEY_B64 |
GitHub authentication (clone/push/pull over SSH) | ~/.ssh/id_aab_auth |
Yes — managed block wires github.com → IdentityFile |
No |
GIT_SIGNING_PRIVATE_KEY_B64 |
git commit / tag signing | ~/.ssh/id_aab_signing |
No | Yes — gpg.format=ssh, user.signingkey, commit.gpgsign=true, tag.gpgsign=true |
Keeping them separate lets you:
- Use an existing GitHub auth identity (provisioned by SSO, a password manager, or a hardware key) while the bootstrap manages only the signing key.
- Rotate one role without touching the other.
- Avoid granting read/write access to every repo your GitHub account can reach just because you wanted a signing key installed — the signing key is a low-privilege artifact whose only job is to produce a verifiable signature.
Both can hold the same key if you want, but the two env vars are the recommended way to keep the roles distinct.
GH_AUTH_SSH_PRIVATE_KEY_B64 — wires a managed block into ~/.ssh/config for github.com:
# >>> autonomous-agent-bootstrap >>>
Host github.com
IdentityFile ~/.ssh/id_aab_auth
IdentitiesOnly yes
# <<< autonomous-agent-bootstrap <<<
Pre-existing entries in ~/.ssh/config (other Host blocks, IdentityFile lines for other hosts) are preserved — re-runs rewrite only the managed block between the marker pair.
GIT_SIGNING_PRIVATE_KEY_B64 — sets the following in ~/.gitconfig via git config --global:
gpg.format = ssh
user.signingkey = ~/.ssh/id_aab_signing.pub
commit.gpgsign = true
tag.gpgsign = true
If you don't want every commit/tag signed, drop commit.gpgsign / tag.gpgsign after bootstrap (git config --global --unset commit.gpgsign, etc.), or flip them to false. The key on disk stays put; only the auto-signing preference changes.
Generate a new ed25519 key (passphrase omitted so the bootstrap can read it non-interactively), then base64-encode the private key:
ssh-keygen -t ed25519 -C "you@example.com" -f ~/.ssh/new_key -N ""
base64 -w0 < ~/.ssh/new_key # Linux (GNU coreutils)
base64 < ~/.ssh/new_key | tr -d '\n' # macOS / BSDCopy the single-line output and set it on whichever env var matches the role:
export GH_AUTH_SSH_PRIVATE_KEY_B64="AAAA...==" # auth identity
export GIT_SIGNING_PRIVATE_KEY_B64="AAAA...==" # signing keyUpload the matching public key (~/.ssh/new_key.pub) to GitHub under Settings → SSH and GPG keys → New SSH key. GitHub lets you choose the key type:
- Authentication Key — for
git clone git@github.com:…,git pushover SSH, etc. Use this for the auth key. - Signing Key — for GitHub to display ✅ next to signed commits and tags. Use this for the signing key.
You can upload the same public key under both types if you want a single blob to serve both roles. You can also upload different keys for each — this is the recommended setup if the auth identity is shared with other tooling (e.g. SSO-provisioned) and shouldn't double as a signing artifact.
| Path | How |
|---|---|
~/.local/bin/claude (+ ~/.local/bin/env) |
Written by the Claude Code native installer. |
~/.claude/settings.json |
Overwritten with unattended-mode defaults, then merged with extraKnownMarketplaces / enabledPlugins entries for each plugin in claude_code_plugins.txt. Existing file backed up to settings.json.bak.<timestamp> before the rewrite. |
~/.claude.json |
Merged — hasCompletedOnboarding=true and optional customApiKeyResponses.approved entry. Existing file backed up to .claude.json.bak.<timestamp>. |
~/.bashrc |
Managed block between # >>> autonomous-agent-bootstrap >>> and # <<< autonomous-agent-bootstrap <<<. Rewritten wholesale on every run. |
~/.gitconfig |
user.name, user.email, and credential.https://github.xm233.cn.helper. When GIT_SIGNING_PRIVATE_KEY_B64 is set, also gpg.format=ssh, user.signingkey=~/.ssh/id_aab_signing.pub, commit.gpgsign=true, tag.gpgsign=true. |
~/.ssh/id_aab_auth, ~/.ssh/id_aab_auth.pub |
Written only when GH_AUTH_SSH_PRIVATE_KEY_B64 is set. Private key mode 0600, public key mode 0644, ~/.ssh dir mode 0700. |
~/.ssh/id_aab_signing, ~/.ssh/id_aab_signing.pub |
Written only when GIT_SIGNING_PRIVATE_KEY_B64 is set. Same mode layout as the auth pair. |
~/.ssh/config |
Managed block (same # >>> … <<< marker pair as ~/.bashrc) mapping github.com to ~/.ssh/id_aab_auth. Only touched when GH_AUTH_SSH_PRIVATE_KEY_B64 is set — the signing-only flow leaves ~/.ssh/config alone. Pre-existing entries outside the managed block are preserved. |
| System-wide | gh package, its apt source + signing keyring (requires sudo; script skips with a warning if passwordless sudo isn't available). openssh-client is also installed on demand when either SSH-key env var is set and ssh-keygen isn't already available. |
Safe to re-run. Each run matches the current environment:
- The
~/.bashrcmanaged block is replaced, not appended — so re-running withoutANTHROPIC_API_KEY/GH_TOKENset drops a previously-written export. If you want an export to persist across re-runs, keep the env var set when you re-run. settings.jsonand.claude.jsonare backed up (timestamped.bak) before being rewritten.ghandclaudeare skipped if already installed.git config --globalis only touched for variables that are set.- The
~/.ssh/configmanaged block is replaced in place on re-run; pre-existing entries outside the block are preserved. Re-running withoutGH_AUTH_SSH_PRIVATE_KEY_B64set leaves~/.ssh/configuntouched — the block is not removed automatically. To turn signing off, usegit config --global --unset commit.gpgsign(and similar) after droppingGIT_SIGNING_PRIVATE_KEY_B64.
All tests are driven by a single entry point, ./test.bash. .github/workflows/ci.yml calls the same flags, so "passes locally" == "will pass CI."
./test.bash # lint + unit (default; fast, no side effects)
./test.bash --lint # bash -n + shellcheck
./test.bash --unit # bats suite in tests/
./test.bash --e2e # runs bootstrap.bash on THIS host + assertions — see warning below
./test.bash --docker # same as --e2e, but inside a fresh ubuntu:22.04 container
./test.bash --secrets # gitleaks scan of full history + working tree
./test.bash --all # lint + unit + e2e + secrets, in order--e2e is destructive. It invokes bootstrap.bash for real against the current $HOME: overwrites ~/.claude/settings.json, rewrites the ~/.bashrc managed block, modifies global git config, and installs claude / brev / gh. Only run it on a disposable VM or container (which is how CI exercises it). --docker is the safe alternative — it does the same run inside a throwaway ubuntu:22.04 container, and also serves as the stronger check that bootstrap.bash works against a bare image with nothing pre-installed.
Install the test prerequisites on Ubuntu/Debian with:
sudo apt-get install -y bats shellcheck python3
# gitleaks (v8.18.4, matching CI)
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v8.18.4/gitleaks_8.18.4_linux_x64.tar.gz" \
| sudo tar -xz -C /usr/local/bin gitleaks