Skip to content

brycelelbach/autonomous-agent-bootstrap

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

22 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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
    • 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 github.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://github.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://github.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://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.

4. Config file

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

Supported 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 between export and 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.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.

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_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-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.xm233.cnIdentityFile 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.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.

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.

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