Idempotent shell-script installers that bootstrap a fresh Ubuntu 22.04 VPS for one of four AI-agent stacks, with a one-profile-per-host mutex, shared base tooling, DB clients, headless browser, MCP servers, plugins, and an Obsidian-vault workspace shared across every CLI agent.
You rent a Lightsail/Hetzner/DO box. You want one of these stacks on it, fully configured, in a single command:
| Profile | What you get |
|---|---|
cli-bundle |
Claude Code + OpenAI Codex + Google Antigravity + Cursor + OpenCode + OpenViking CLIs (any combination) |
openclaw |
OpenClaw — local agent gateway on port 18789 |
hermes |
Hermes Agent (Nous Research, Python+uv) |
paperclip |
Paperclip — Node API + embedded Postgres :3100 |
Plus, on every profile:
- Common Linux toolkit (
tmux,git,vim,jq,ripgrep,fd,fzf,htop, …) psql(PostgreSQL client) andclickhouse-clientfrom the official ClickHouse deb repo- Headless browser (
chromiumvia apt — falls back to Playwright's bundled Chromium) - Timezone, swap file, npm user-global prefix,
~/.bashrcPATH - A mutex marker so you don't accidentally stack two agent runtimes on one host
cli-bundle additionally bundles:
- MCP servers for Claude (Context7, Linear, Slack, GitHub, Supabase, Sentry, Notion, Playwright, Filesystem, Obsidian)
- Plugins — superpowers, OpenSpec, plus the official Anthropic marketplace (Linear/Slack/GitHub/Notion/Atlassian/Asana/Figma/Sentry/Supabase/Vercel)
- Obsidian vault — shared workspace at
$OBSIDIAN_VAULT_DIR, all CLIs read/write, optional git auto-sync - Dream mode — cron-driven memory consolidation for Claude
harness/
├── install.sh # top-level launcher (menu, arg, --status, --force)
├── lib/
│ ├── common.sh # mutex_check, mutex_set, load_env, banner
│ ├── base-packages.sh # install_base_packages, install_db_clients,
│ │ # install_headless_browser, install_node,
│ │ # install_pnpm, install_uv
│ ├── obsidian.sh # setup_vault, sync_vault_now/install/uninstall
│ └── plugins.sh # claude_headless, install_official_claude_plugin,
│ # install_claude_plugin, install_openspec,
│ # print_manual_install_hint
└── profiles/
├── cli-bundle/ # 01-system → 02-claude → 03-codex → 04-antigravity
│ # → 05-cursor → 05b-opencode → 05c-openviking → 08-obsidian
│ # → 06-mcp → 07-dream → 09-plugins
├── openclaw/ # 01-system → 02-openclaw
├── hermes/ # 01-system → 02-hermes
└── paperclip/ # 01-system → 02-paperclip
Per-profile docs live in each profiles/<name>/README.md. Architecture +
rationale in .claude/PLAN.md. Decision log in
.claude/CHANGELOG.md. Open design decisions in
.claude/RECOMMENDATIONS.md. Open work items
in .claude/TODO.md.
git clone https://github.com/govtech42/harness.git ~/harness
cd ~/harness
cp profiles/<profile>/.env.example profiles/<profile>/.env
nano profiles/<profile>/.env # fill provider keys, toggles, etc.
./install.sh # interactive menu
# or jump straight in:
./install.sh cli-bundleExample menu:
No profile installed yet.
Pick a profile to install:
1) cli-bundle
2) openclaw
3) hermes
4) paperclip
5) Quit
#?
./install.sh # interactive picker
./install.sh <profile> # install named profile
./install.sh <profile> --force # override mutex (see below)
./install.sh --status # show installed profile, if any
./install.sh --help # usage<profile> is one of: cli-bundle, openclaw, hermes, paperclip.
The launcher writes the profile name to ~/.harness-profile (chmod 600) when
an install completes. Any subsequent run for a different profile refuses
with exit code 2:
ERROR: profile 'cli-bundle' already installed on this host.
Refusing to install 'openclaw'.
Override: --force (not recommended; profiles are not designed to coexist)
Why: these stacks compete for PATH entries, ports, systemd unit names, and
memory on small VPSes. Coexistence isn't supported. If you really know
better, --force bypasses the check.
The cli-bundle profile is the exception: it intentionally stacks six
thin CLI clients (Claude, Codex, Antigravity, Cursor, OpenCode, OpenViking)
which have disjoint config dirs and no port binds.
| Requirement | Detail |
|---|---|
| OS | Ubuntu 22.04 LTS (other Debian-likes may work but are untested) |
| RAM | 2 GB minimum; 4 GB recommended for cli-bundle with MCPs + plugins |
| Disk | 10 GB free |
| Network | Outbound HTTPS to npm, GitHub, ClickHouse repo, each agent provider |
| Inbound | Static IP / SSH on :22; OAuth callbacks for some agents |
| User | ubuntu (or any user with sudo and a writable $HOME) |
Six CLIs — toggle each in .env. Defaults: Claude true, others false.
| Toggle | CLI | Install path |
|---|---|---|
INSTALL_CLAUDE |
Claude Code | npm i -g @anthropic-ai/claude-code |
INSTALL_CODEX |
OpenAI Codex | npm i -g @openai/codex |
INSTALL_ANTIGRAVITY |
Google Antigravity | upstream installer (curl … | bash) |
INSTALL_CURSOR |
Cursor agent | upstream installer (curl … | bash) |
INSTALL_OPENCODE |
OpenCode | upstream installer (curl -fsSL https://opencode.ai/install | bash) |
INSTALL_OPENVIKING |
OpenViking (ov) |
npm i -g @openviking/cli |
Plus:
- MCP servers registered for Claude (
06-mcp.sh) — Context7, Linear, Slack, GitHub, Supabase, Sentry, Notion, Playwright, Filesystem, Obsidian. - Plugins (
09-plugins.sh) — see Plugins below. - Obsidian vault (
08-obsidian.sh) — shared workspace for every CLI. - Dream mode (
07-dream.sh) — cron-drivenclaude -pinvocation ofanthropic-skills:consolidate-memoryat03:00; log auto-rotates at 5 MiB.
Full docs: profiles/cli-bundle/README.md.
Installs openclaw from npm globally. Seeds ~/.openclaw/openclaw.json with
your default model. Optionally onboards the systemd daemon
(OPENCLAW_AS_SERVICE=true).
Defaults to the upstream curl|bash installer
(HERMES_USE_UPSTREAM_INSTALLER=true), which sets up Python 3.11 via uv and
puts hermes on PATH. Dev path (false) does git clone + setup-hermes.sh.
git clone + pnpm install + pnpm build. Embedded PostgreSQL is provisioned by
the app on first run; no external DB needed. Optional systemd unit for the API
server on port 3100.
Available in cli-bundle via 09-plugins.sh. Headless install where the CLI
supports it; printed manual hint otherwise.
| Plugin | Claude | Codex | Antigravity | Cursor | OpenCode |
|---|---|---|---|---|---|
| superpowers | headless | manual /plugins |
manual (not documented) | manual /add-plugin |
manual fetch URL |
| OpenSpec | universal (npm global, /opsx:* slash commands from any CLI) — invoked via openspec init per project |
||||
| agent-skills | non-interactive CLI install across claude-code / codex / cursor / opencode — curated, security-validated skill registry |
agent-skills is a curated,
security-validated skill registry. INSTALL_AGENT_SKILLS=true installs a
default set tuned for web/mobile product work (Next.js + React Native, NestJS +
Nx monorepo, accessibility + security) globally to every installed CLI that
supports it. Override the skill list with AGENT_SKILLS_LIST and the target
CLIs with AGENT_SKILLS_AGENTS in .env. Browse the catalog with
npx @tech-leads-club/agent-skills list.
Plus the official Anthropic marketplace (Claude-only, bundles pre-configured MCP + skills + slash commands):
INSTALL_LINEAR_PLUGIN, INSTALL_SLACK_PLUGIN, INSTALL_GITHUB_PLUGIN,
INSTALL_NOTION_PLUGIN, INSTALL_ATLASSIAN_PLUGIN, INSTALL_ASANA_PLUGIN,
INSTALL_FIGMA_PLUGIN, INSTALL_SENTRY_PLUGIN, INSTALL_SUPABASE_PLUGIN,
INSTALL_VERCEL_PLUGIN.
These coexist with the raw MCP toggles in 06-mcp.sh (e.g. INSTALL_LINEAR=true
registers the bare MCP endpoint); the official plugin is the richer path.
Opt-in shared workspace every CLI reads and writes. Headless on VPS (no Obsidian app required); same files render in the desktop app on a workstation that pulls the git remote.
INSTALL_OBSIDIAN=true
OBSIDIAN_VAULT_DIR=/home/ubuntu/vault
OBSIDIAN_VAULT_REPO=git@github.com:you/your-vault.git # optional
OBSIDIAN_AUTOSYNC=true # opt-in
OBSIDIAN_AUTOSYNC_SCHEDULE=*/15 * * * *Layout:
$OBSIDIAN_VAULT_DIR/
├── .claude/log.md # Claude Code writes here
├── .codex/log.md # Codex
├── .antigravity/log.md # Antigravity
├── .cursor/log.md # Cursor
├── inbox.md # any agent appends
├── notes/ # main markdown
├── .obsidian/app.json # config seed (versioned if git)
└── .gitignore # excludes workspace.json, cache, .trash/
Git auto-sync uses -X ours — local edits always win on conflict
(trade-off documented in .claude/RECOMMENDATIONS.md and .claude/CHANGELOG.md).
Every profile's 01-system.sh installs:
postgresql-client(Ubuntu apt) →psqlclickhouse-clientfrom the official ClickHouse deb repo (signed keyring at/etc/apt/keyrings/clickhouse-keyring.gpg)
Toggle off with INSTALL_DB_CLIENTS=false in the profile's .env. Granular
control: INSTALL_POSTGRES_CLIENT, INSTALL_CLICKHOUSE_CLIENT.
install_headless_browser() in lib/base-packages.sh tries chromium-browser
then chromium via apt; warns (does not fail) if no apt package resolves.
Used for general agent shell work. Default INSTALL_HEADLESS_BROWSER=true.
Playwright is a separate concern: INSTALL_PLAYWRIGHT=true in 06-mcp.sh
does its own playwright install-deps + bundled-Chromium download +
@playwright/mcp@latest MCP registration.
- Each profile's
.env(copied from.env.example) holds provider keys and toggles. The loaderchmod 600s it on first run. ANTHROPIC_API_KEY(and friends) get appended to~/.bashrc; the file is re-chmoded to600after the write.~/.harness-profileischmod 600.- Tokens (OAuth, etc.) live under
~/.claude/,~/.openclaw/, etc. — back these up before destroying the VPS:tar czf harness-backup.tgz ~/.claude ~/.openclaw ~/.opencode \ ~/.codex ~/.cursor ~/.antigravity ~/vault ~/.harness-profile
# show installed profile
./install.sh --status
# re-run a profile (idempotent — re-applies .env changes)
./install.sh <profile>
# wipe the marker if you intend a clean swap (combine with --force)
rm ~/.harness-profile && ./install.sh <other-profile>
# update an npm-global CLI
npm update -g @anthropic-ai/claude-code
npm update -g @openai/codex
npm update -g openclaw # for the openclaw profile
npm update -g @fission-ai/openspec # if INSTALL_OPENSPEC=true
# manual Obsidian sync (if INSTALL_OBSIDIAN + git repo configured)
source ~/.bashrc && obsidian-vault-syncLocal test harness in tests/:
# install deps (Ubuntu/Debian)
sudo apt-get install -y shellcheck bats
# install deps (macOS)
brew install shellcheck bats-core
bash tests/lint.sh
bash tests/check_env_completeness.sh
bats tests/GitHub Actions:
ci.yml— runslint,env-completeness, andbatsjobs in parallel on every push and PR.release.yml— onv*tag, reruns CI then publishes a GitHub release with notes extracted from.claude/CHANGELOG.md.
| Symptom | Fix |
|---|---|
command not found: claude (or any CLI) |
source ~/.bashrc (PATH update lives there) |
Refusing to install '<other>' |
Expected — see Mutex |
| MCP OAuth callback never returns | Open the URL on your laptop, not on the VPS |
| Playwright MCP install fails | sudo npx playwright install-deps |
| Out of RAM mid-install | Bump SWAP_SIZE_GB in .env, re-run |
| ClickHouse repo signature error | Delete /etc/apt/keyrings/clickhouse-keyring.gpg and re-run |
| Cron Dream job logs nothing | crontab -l | grep claude-dream to confirm registered |
| Obsidian auto-sync fails | tail ~/vault/.claude/sync.log; verify OBSIDIAN_VAULT_REPO push access |
Plugin install hangs in claude -p |
Check ~/.claude/logs/ and retry with --dangerously-skip-permissions |
chromium package missing |
Rely on Playwright's bundled Chromium (set INSTALL_PLAYWRIGHT=true) |
# syntax-check every shell script
find . -name '*.sh' -exec bash -n {} \; -print
# shellcheck (optional)
find . -name '*.sh' -exec shellcheck -x -S warning -e SC1090,SC1091 {} \;Conventions baked into every script:
set -euo pipefail- Idempotent (
mkdir -p,grep -qguards,git pull --ff-only,claude mcp removebeforeadd) - Pure bash, no perl/python dependencies for the launcher itself
.envalways sourced via the sharedload_envhelper
| Profile | Install path | Real-VPS verified |
|---|---|---|
| cli-bundle | npm global + upstream installers (5 CLIs + plugins) | ✅ (partial) |
| openclaw | npm i -g openclaw |
|
| hermes | upstream curl | bash |
|
| paperclip | git clone + pnpm build |
Releases: v0.6.1 (latest) — see .claude/CHANGELOG.md for the full reverse-chrono history.
Open design decisions awaiting review: .claude/RECOMMENDATIONS.md.
Open work items: .claude/TODO.md.
MIT — see LICENSE.md.