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- Edit / Write / Read for
~/.claude/**and~/.claude.jsonpre-approved inpermissions.allowso the agent can update its own config, agents, skills, and memory files without a prompt even when bypass mode is toggled off mid-session - 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.comapt repo (the distro-shippedghpredatesgh auth token/gh auth git-credential). -
git —
user.name/user.emailset from env, andghregistered as thegithub.comcredential 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.comin 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 unauthenticatedraw.githubusercontent.comotherwise. 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://raw.githubusercontent.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://raw.githubusercontent.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://raw.githubusercontent.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, or pipe them on stdin:
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, by path:
bash bootstrap.bash /tmp/aab.conf
# From a local checkout, by stdin (heredoc, redirect, or any non-TTY pipe):
bash bootstrap.bash <<'CONF'
AAB_CLAUDE_CODE_MODEL=claude-opus-4-7
ANTHROPIC_API_KEY=...
GH_TOKEN=...
CONF
# From curl-pipe-bash with a positional path. The `-s --` hands the
# positional arg through to the piped script:
curl -fsSL https://raw.githubusercontent.com/brycelelbach/autonomous-agent-bootstrap/main/bootstrap.bash | bash -s -- /tmp/aab.conf
# From curl-pipe-bash with stdin. Process substitution (`bash <(...)`)
# frees stdin for the heredoc — the curl-pipe-bash form (`curl ... |
# bash`) leaves stdin attached to the closed curl pipe, so heredocs
# don't reach the script:
bash <(curl -fsSL https://raw.githubusercontent.com/brycelelbach/autonomous-agent-bootstrap/main/bootstrap.bash) <<'CONF'
AAB_CLAUDE_CODE_MODEL=claude-opus-4-7
ANTHROPIC_API_KEY=...
GH_TOKEN=...
CONFThe file (or stdin) is sourced via set -a; . file; set +a, so it has full access to bash syntax:
KEY=value,KEY="value with spaces",KEY='single quoted'KEY="${OTHER:-default}"and other parameter-expansion formsKEY="$(some-cmd)"command substitutions\-quoted multi-line values- optional leading
export(withKEY=valuesemantics either way) #line comments anywhere; blank lines are ignored
Values containing shell metacharacters (&, |, ;, $, *, (, )) need to be quoted — bash treats URL=https://x.com/?a=b&c=d as backgrounded URL=...& plus c=d, not a single value. Wrap the right-hand side in "..." or '...' whenever it contains anything that isn't [A-Za-z0-9_./:%@-].
The flip side: a malformed config file aborts the bootstrap rather than silently warning. A typo'd line or one that runs an unknown command short-circuits the run on the first error, which is the safer default for a credentials-loading step.
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. An empty stdin (no positional path, TTY-attached or no data piped) is treated the same as recipe 1–3: the bootstrap runs with whatever env vars are already set in the shell.
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_HAIKU_MODEL |
Unprefixed haiku-tier model name. Claude Code uses this tier for background tasks (web search, summarization). Exported as ANTHROPIC_DEFAULT_HAIKU_MODEL — raw in the anthropic branch, prefixed with AAB_CLAUDE_CODE_MODEL_THIRD_PARTY_PREFIX in the third-party branch. Defaults to claude-haiku-4-5. |
AAB_CLAUDE_CODE_SONNET_MODEL |
Unprefixed sonnet-tier model name, used when /model selects the sonnet tier mid-session. Exported as ANTHROPIC_DEFAULT_SONNET_MODEL with the same prefix-or-not treatment. Defaults to claude-sonnet-4-6. |
AAB_CLAUDE_CODE_OPUS_MODEL |
Unprefixed opus-tier model name, used when /model selects the opus tier mid-session. Exported as ANTHROPIC_DEFAULT_OPUS_MODEL with the same prefix-or-not treatment. Defaults to claude-opus-4-7. |
AAB_CLAUDE_CODE_MODEL_THIRD_PARTY_PREFIX |
Prepended to every per-tier model name when exporting ANTHROPIC_MODEL / ANTHROPIC_DEFAULT_*_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.com.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. |
/etc/environment |
Managed block (same # >>> … <<< marker pair) mirroring the resolved provider / model / token state into a KEY=VALUE file PAM loads for every session. Pre-existing entries outside the block are preserved; re-runs replace the block in place. Requires sudo; the bootstrap warns and skips this step if passwordless sudo isn't available. |
| 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. - The
/etc/environmentmanaged block is replaced in place on re-run, mirroring the same resolved-at-bootstrap-time provider / model / token state that goes into~/.bashrc. The runtimeclaude_code_switch_inference_providershell function only updates~/.bashrc(interactive sessions); to make a switch visible to non-interactive shells (ssh remote command, systemdEnvironmentFile=), re-run the bootstrap with the new provider.
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