Skip to content

brycelelbach/autonomous-agent-bootstrap

Repository files navigation

autonomous-agent-bootstrap

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.

What it sets up

  1. Claude Code — installed via the official native installer, then configured for unattended use:

    • bypassPermissions default mode, skipDangerousModePermissionPrompt, sandboxed
    • Edit / Write / Read for ~/.claude/** and ~/.claude.json pre-approved in permissions.allow so 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 to claude-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_KEY pre-approved if provided (no first-run approval prompt)
    • claude aliased to claude --dangerously-skip-permissions in interactive shells
  2. gh CLI — latest release from the official cli.github.com apt repo (the distro-shipped gh predates gh auth token / gh auth git-credential).

  3. gituser.name / user.email set from env, and gh registered as the github.com credential helper so git clone / git push reuse the gh-stored token with no interactive prompt. If GIT_SIGNING_PRIVATE_KEY_B64 is set, git is also configured to sign every commit and tag with that key (see SSH keys).

  4. 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 the IdentityFile for github.com in 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's user.signingkey / commit.gpgsign / tag.gpgsign config. Does not touch ~/.ssh/config.

    See SSH keys for how to generate, encode, and upload them.

  5. Claude Code plugins — marketplaces listed in claude_code_plugins.txt are registered in ~/.claude/settings.json's extraKnownMarketplaces, and the plugins they declare are flipped on in enabledPlugins. 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 via gh api when gh is authenticated (picks up GH_TOKEN or gh auth login credentials) and falls back to unauthenticated raw.githubusercontent.com otherwise. Entries the caller lacks access to are logged and skipped; they do not fail the bootstrap.

Requirements

To run the bootstrap:

  • Ubuntu/Debian host with bash and apt-get
  • A bare ubuntu:22.04 container image is a valid starting point — everything else (curl, python3, git, sudo, ca-certificates, and gh) 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):

  • bash
  • shellcheck — for lint
  • bats (≥1.2) and python3 — for the unit suite
  • gitleaks (pinned to v8.18.4 in CI) — for the secret scan
  • docker — for the bare-container end-to-end check
  • The on-host --e2e job doesn't need anything beyond bash; the bootstrap it invokes installs its own prerequisites

Quick start

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.

1. First-party + third-party (both credentials, pick a default)

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"

2. First-party only

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"

3. Third-party only

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.

4. Config file or stdin

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=...
CONF

The 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 forms
  • KEY="$(some-cmd)" command substitutions
  • \-quoted multi-line values
  • optional leading export (with KEY=value semantics 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.conf

A 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.

Switching inference providers

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-party

The if/else in the managed block unsets the other provider's variables, so you won't get cross-provider env pollution.

Environment variables

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-7aws/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.

Managing the plugin list

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.

SSH keys

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.comIdentityFile No
GIT_SIGNING_PRIVATE_KEY_B64 git commit / tag signing ~/.ssh/id_aab_signing No Yesgpg.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.

What each role writes

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.

Generating and encoding a key

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 / BSD

Copy 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 key

Upload 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 push over 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.

What the script touches

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.

Re-running

Safe to re-run. Each run matches the current environment:

  • The ~/.bashrc managed block is replaced, not appended — so re-running without ANTHROPIC_API_KEY / GH_TOKEN set 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.json and .claude.json are backed up (timestamped .bak) before being rewritten.
  • gh and claude are skipped if already installed.
  • git config --global is only touched for variables that are set.
  • The ~/.ssh/config managed block is replaced in place on re-run; pre-existing entries outside the block are preserved. Re-running without GH_AUTH_SSH_PRIVATE_KEY_B64 set leaves ~/.ssh/config untouched — the block is not removed automatically. To turn signing off, use git config --global --unset commit.gpgsign (and similar) after dropping GIT_SIGNING_PRIVATE_KEY_B64.
  • The /etc/environment managed block is replaced in place on re-run, mirroring the same resolved-at-bootstrap-time provider / model / token state that goes into ~/.bashrc. The runtime claude_code_switch_inference_provider shell function only updates ~/.bashrc (interactive sessions); to make a switch visible to non-interactive shells (ssh remote command, systemd EnvironmentFile=), re-run the bootstrap with the new provider.

Running the tests

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

About

Script for installing claude code and setting it up to run unattended.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages