Prerequisites & install
Budget roughly 60–90 minutes for a first install. The canonical path uses uv for the Python toolchain; a venv + pip fallback is documented at the end.
1. System & OS
HEADING OS runs inside a Linux userland. Three supported shapes:
- Windows 11 + WSL2 (Ubuntu) — the reference platform; all docs assume it.
- macOS 13+ — supported (VPN and browser steps are GUI-only).
- Native Linux — supported.
Python version. pyproject.toml declares requires-python = ">=3.11", so Python 3.11 or newer is the floor. uv installs a conforming Python for you — a system Python is not required.
cmd.exe to work around a Linux problem. Keep all repositories on the Linux filesystem (~/...), never on /mnt/c: the Windows mount is far slower for git/Python and has permission quirks. A PS C:\> prompt means you are in PowerShell; enter Ubuntu with wsl -d Ubuntu.
Windows-only WSL bootstrap (one time, Administrator PowerShell)
wsl --install # reboot when prompted; create a Linux user + password on first launch
If WSL already exists this reports "distribution already exists" — skip it, then run wsl -d Ubuntu.
2. Python toolchain
pyproject.toml is the source of truth for dependencies; uv.lock hash-pins the transitive set. Use uv, not system pip — recent Ubuntu ships only python3 with no pip, by design. Every script runs as uv run python scripts/....
System packages + toolchain (inside Ubuntu / macOS / Linux)
# system packages
sudo apt update && sudo apt install -y build-essential curl git ca-certificates gh
# Node via nvm (needed for the Claude CLI)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
export NVM_DIR="$HOME/.nvm" && . "$NVM_DIR/nvm.sh"
nvm install --lts
# uv (Python manager — installs Python for you)
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
uv --version
# Claude CLI
npm install -g @anthropic-ai/claude-code
which claude # must resolve under ~/.nvm/..., NOT /mnt/c/...
Authenticate inside Linux (Windows logins do not carry into WSL)
gh auth login # GitHub.com -> HTTPS -> Yes -> Login with a web browser
claude # pick a theme, then "Claude account with subscription"
Install dependencies (canonical path)
cd <engine>
uv sync # Python + all dependencies into the project environment
Runtime stack of note (exact pins in pyproject.toml): anthropic, exchangelib (Exchange mail), Telethon (Telegram), playwright, weasyprint + liteparse + markitdown (documents/PDF), the Google API clients, fastapi + uvicorn + watchdog + apscheduler (bridge daemon), plus firecrawl-py, youtube-transcript-api, yt-dlp, replicate, openai, google-genai, langfuse. Dev tooling: pytest, bandit, ruff, pre-commit, detect-secrets, pip-audit.
3. .env configuration
Create your env file from the template at the engine root — it is gitignored and never committed:
cd <engine>
cp .env.example .env
Every variable, with purpose. Feature-gated variables are optional — the feature simply stays dark until the key is set.
| Variable | Purpose | Required? |
|---|---|---|
ANTHROPIC_API_KEY | Claude API key (alternative to a subscription login via the Claude CLI). | One of the two |
GH_TOKEN | GitHub token used for all git push / backup auth. Without it, backups cannot authenticate. | Required for backup |
HEADING_OS_DATA | Absolute path to the data overlay. Only needed if the sibling .heading-os-data is not auto-discovered. | Optional |
HEADING_OS_TZ | IANA timezone for all timestamps, calendar, and daemon scheduling. Engine default is UTC. | Recommended |
OLLAMA_API_KEY | Auth for Ollama cloud models. Local embeddings do not need it. | Optional |
EXCHANGE_EMAIL / _PASSWORD / _SERVER / _USERNAME / _AUTH_TYPE / _TIMEZONE | On-prem Microsoft Exchange mailbox (email + calendar skills). | Optional* |
TELEGRAM_API_ID / _API_HASH / _PHONE | Telegram client credentials (/telegram, /viraid). | Optional* |
PERPLEXITY_API_KEY | Perplexity — deep-research web acquisition. | Optional |
TAVILY_API_KEY / BRAVE_API_KEY | Web search (Tavily primary, Brave Search fallback) for /osint. | Optional |
FIRECRAWL_API_KEY | Firecrawl web scraping. | Optional |
CONTEXT7_API_KEY | Context7 library-docs lookups. | Optional |
REPLICATE_API_TOKEN | Replicate image generation (/flux-image, /design). | Optional |
DEHASHED_* / HIBP_API_KEY / HUNTER_API_KEY / VIRUSTOTAL_API_KEY | OSINT data sources. | Optional |
* Feature-gated: the matching skill stays inactive until the credentials are present.
.env (or a password manager). The commit hooks and an unbypassable push-time content scan are designed to block secrets, but the first line of defense is you. Never pass git commit --no-verify.
4. Ollama — local semantic recall
Ollama is a local LLM runtime used for on-machine embeddings that power the workspace's semantic recall and memory index. Zero API cost; nothing leaves the machine on the local path.
Model used and why. config/memory-index.yaml sets model: bge-m3 — a multilingual (1024-dim) embedding model used by scripts/memory-index.py and the /recall skill (hybrid dense bge-m3 cosine + BM25 lexical). It is embeddings-only, not a chat model.
# install ollama (official installer)
curl -fsSL https://ollama.com/install.sh | sh
# pull the embedding model used by recall / memory-index
ollama pull bge-m3
# (optional) a cloud reasoning model for /council + /deep-research-advance
ollama pull kimi-k2.6:cloud # auth via OLLAMA_API_KEY in the engine .env
:cloud model — those prompts leave the machine. The third-party clouds are treated as one privacy tier; cloud is for non-sensitive work only.
memory-index.py build embeds the whole corpus through bge-m3 and takes roughly 85 minutes on CPU (~7,300 chunks at ~0.7s each). It is not a hang — it commits per file with live + path [NN%] progress and is resumable; later runs are incremental and fast. Run it with a long/no timeout, ideally in the background. A GPU host cuts the time sharply but is not required.
cd <engine>
uv run python scripts/memory-index.py build # ~85 min first run, resumable
5. Brave — the automation browser
A dedicated Brave browser with a separate ClaudeCode profile is the canonical browser for all workspace browser work: CDP-driven automation (scripts/browser.py attaches over CDP), cookie extraction (scripts/utils/chromium_cookies.py), yt-dlp --cookies-from-browser brave:ClaudeCode, and the /setup-browser-cookies skill.
Why a dedicated profile, not your personal browser. The automation never uses your personal browser, and never asks you to log in from it — keeping the assistant's authenticated sessions isolated from your own. Why CDP-attach: launching Brave under Playwright's persistent-context mode is unstable (the controlled tab closes immediately); attaching over CDP to an externally launched browser is stable. Never call browser.close() on a CDP-attached session — it kills the whole browser.
# Brave via its official apt repo (Linux/WSL)
sudo apt install brave-browser
which brave-browser # /usr/bin/brave-browser
Brave is machine-local — not carried by the repos. Reinstall after every fresh clone or relocation (same class as plugins and gitignored credentials).
WSL "open URL" routing (Windows installs)
WSL has no working default browser, and a WSL-native Brave window under WSLg is unreliable for OAuth logins. Route URL opens to the Windows browser via wslview (user-level, no sudo):
xdg-settings set default-web-browser wslview.desktop
xdg-mime default wslview.desktop x-scheme-handler/http x-scheme-handler/https
This makes Claude Code MCP / connector OAuth logins hand off cleanly to the Windows browser. The canonical automation browser stays the CDP-driven Brave (no visible window).
cli.js path, never npx — on WSL npx/node on PATH resolve to the Windows install and the JSON-RPC stdio dies on initialize. (The local /playwright skill uses the Python CDP path and is unaffected.) See Skills, MCP & plugins.
6. VPN preflight (browsing skills)
A runtime gate (.claude/rules/vpn-preflight.md) fires before browsing skills that hit services blacklisting datacenter IPs — YouTube transcripts (/yt-pulse), Google/LinkedIn scraping, some OSINT sources. Datacenter exits (e.g. Mullvad) get blocked; a residential-friendly exit like Proton passes. On Linux the gate can switch directly:
protonvpn-cli connect --cc NL # residential-friendly exit for YouTube/Google
protonvpn-cli status
On Windows/macOS you switch manually in the GUI. This is a runtime gate, not an install step — it exists so users of browsing skills understand why a run pauses.
7. Git hooks / pre-commit (per clone)
.git/hooks is machine-local and not cloned, so the commit-time secret gate must be armed once per fresh clone:
cd <engine>
pre-commit install # writes into .git/hooks; run once per clone
pre-commit run --all-files # optional: run the whole suite immediately
uv run python scripts/install-hooks.py --check # verify the framework is the active manager
The hooks enforce detect-secrets, bandit, ruff security rules, standard checks, a workspace secret scanner, a real-entity/PII content guard, the engine/data leak guards, and a pip-audit CVE scan on dependency changes.
git commit --no-verify skips every commit hook — never pass it. The authoritative, unbypassable gate is the push-time content scan in scripts/push-all.py (pure code, no skip flag, scans both repos before anything leaves the machine).
8. Plugins (reinstall per clone path)
Claude Code plugins are enabled in .claude/settings.json (travels with git), but the installs are tracked per project path (machine-local, never synced). After any clone or relocation you get "enabled but not installed" until you reinstall under the new path:
cd <engine>
claude plugin marketplace add anthropics/claude-plugins-official
claude plugin install superpowers@claude-plugins-official --scope project
claude plugin install skill-creator@claude-plugins-official --scope project
claude plugin install claude-md-management@claude-plugins-official --scope project
claude plugin install frontend-design@claude-plugins-official --scope project
@latest. The form is always <plugin>@<marketplace> (e.g. @claude-plugins-official) — plugin@latest makes Claude look for a marketplace named "latest" and fail. List marketplaces with claude plugin marketplace list.
See Skills, MCP & plugins for the full inventory.
9. The data overlay (two-part topology)
The engine is code only and resolves a separate private data repository at runtime. They sit as siblings under one parent so the engine auto-discovers the data sibling:
~/ai/claude-workspaces/
├── .heading-os/ ← engine (code; eventually public)
├── .heading-os-data/ ← your data (CRM, knowledge, outputs, context — writable)
└── .heading-os-corporate/ ← corporate content (optional; populated by sync-corporate.py)
On a clone without a data overlay, the engine runs on its defaults and *.example.* templates — features needing data stay dark; nothing breaks. If auto-discovery fails, set HEADING_OS_DATA=/abs/path/.heading-os-data in .env.
Create your own data repo (solo deploy, one command)
cd <engine>
uv run python scripts/create-data-repo.py # creates the repo + prints the HEADING_OS_DATA wiring line
Clone existing repos (managed / migration)
cd ~/ai/claude-workspaces
git clone https://github.com/<org>/heading-os.git .heading-os
git clone https://github.com/<org>/heading-os-data-<slug>.git .heading-os-data
git clone https://github.com/<org>/heading-os-corporate.git .heading-os-corporate # optional
Backup (human-gated, with [0 0] verification + pre-push secret scan)
uv run python scripts/push-all.py # commit + push
uv run python scripts/push-all.py --dry-run # preview only
See Data overlay structure for the full layout.
First-run verification
cd <engine>
uv run python scripts/workspace-health.py
uv run python -c "from scripts.utils.paths import get_data_root; print(get_data_root())" # prints your data path
claude # trust the engine folder, then:
/prime
Appendix — venv + pip fallback
The classic route works where uv is unavailable. Note that on stock Ubuntu without pip it will not work out of the box; prefer uv sync.
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt # production deps
pip install -r requirements-dev.txt # dev tooling
pytest -q # verify the suite