diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
new file mode 100644
index 000000000..25e5b050e
--- /dev/null
+++ b/.claude-plugin/marketplace.json
@@ -0,0 +1,30 @@
+{
+ "name": "headroom-marketplace",
+ "owner": {
+ "name": "Headroom Contributors"
+ },
+ "metadata": {
+ "description": "Headroom marketplace for Claude Code and GitHub Copilot CLI plugins.",
+ "version": "0.1.0"
+ },
+ "plugins": [
+ {
+ "name": "headroom",
+ "source": "./plugins/headroom-agent-hooks",
+ "description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
+ "version": "0.1.0",
+ "author": {
+ "name": "Headroom Contributors",
+ "url": "https://github.com/JerrettDavis/headroom"
+ },
+ "homepage": "https://github.com/JerrettDavis/headroom",
+ "repository": "https://github.com/JerrettDavis/headroom",
+ "keywords": [
+ "headroom",
+ "hooks",
+ "claude-code",
+ "copilot-cli"
+ ]
+ }
+ ]
+}
diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json
new file mode 100644
index 000000000..25e5b050e
--- /dev/null
+++ b/.github/plugin/marketplace.json
@@ -0,0 +1,30 @@
+{
+ "name": "headroom-marketplace",
+ "owner": {
+ "name": "Headroom Contributors"
+ },
+ "metadata": {
+ "description": "Headroom marketplace for Claude Code and GitHub Copilot CLI plugins.",
+ "version": "0.1.0"
+ },
+ "plugins": [
+ {
+ "name": "headroom",
+ "source": "./plugins/headroom-agent-hooks",
+ "description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
+ "version": "0.1.0",
+ "author": {
+ "name": "Headroom Contributors",
+ "url": "https://github.com/JerrettDavis/headroom"
+ },
+ "homepage": "https://github.com/JerrettDavis/headroom",
+ "repository": "https://github.com/JerrettDavis/headroom",
+ "keywords": [
+ "headroom",
+ "hooks",
+ "claude-code",
+ "copilot-cli"
+ ]
+ }
+ ]
+}
diff --git a/README.md b/README.md
index bedb39c53..cac4f432e 100644
--- a/README.md
+++ b/README.md
@@ -1,266 +1,274 @@
-
-
-# Headroom
-
-**Compress everything your AI agent reads. Same answers, fraction of the tokens.**
-
-[](https://github.com/chopratejas/headroom/actions/workflows/ci.yml)
-[](https://pypi.org/project/headroom-ai/)
-[](https://www.npmjs.com/package/headroom-ai)
-[](https://huggingface.co/chopratejas/kompress-base)
-[](https://headroomlabs.ai/dashboard)
-[](LICENSE)
-[](https://headroom-docs.vercel.app/docs)
-
-

-
-
-
----
-
-Every tool call, log line, DB read, RAG chunk, and file your agent injects into a prompt is mostly boilerplate. Headroom strips the noise and keeps the signal — **losslessly, locally, and without touching accuracy.**
-
-> **100 logs. One FATAL error buried at position 67. Both runs found it.**
-> Baseline **10,144 tokens** → Headroom **1,260 tokens** — **87% fewer, identical answer.**
-> `python examples/needle_in_haystack_test.py`
-
----
-
-## Quick start
-
-Works with Anthropic, OpenAI, Google, Bedrock, Vertex, Azure, OpenRouter, and 100+ models via LiteLLM.
-
-**Wrap your coding agent — one command:**
-
-```bash
-pip install "headroom-ai[all]"
-
-headroom wrap claude # Claude Code
-headroom wrap codex # Codex
-headroom wrap cursor # Cursor
-headroom wrap aider # Aider
-headroom wrap copilot # GitHub Copilot CLI
-```
-
-**Drop it into your own code — Python or TypeScript:**
-
-```python
-from headroom import compress
-
-result = compress(messages, model="claude-sonnet-4-5")
-response = client.messages.create(model="claude-sonnet-4-5", messages=result.messages)
-print(f"Saved {result.tokens_saved} tokens ({result.compression_ratio:.0%})")
-```
-
-```typescript
-import { compress } from 'headroom-ai';
-const result = await compress(messages, { model: 'gpt-4o' });
-```
-
-**Or run it as a proxy — zero code changes, any language:**
-
-```bash
-headroom proxy --port 8787
-ANTHROPIC_BASE_URL=http://localhost:8787 your-app
-OPENAI_BASE_URL=http://localhost:8787/v1 your-app
-```
-
----
-
-## Why Headroom
-
-- **Accuracy-preserving.** GSM8K **0.870 → 0.870** (±0.000). TruthfulQA **+0.030**. SQuAD v2 and BFCL both **97%** accuracy after compression. Validated on public OSS benchmarks you can rerun yourself.
-- **Runs on your machine.** No cloud API, no data egress. Compression latency is milliseconds — faster end-to-end for Sonnet / Opus / GPT-4 class models than a hosted service round-trip.
-- **[Kompress-base](https://huggingface.co/chopratejas/kompress-base) on HuggingFace.** Our open-source text compressor, fine-tuned on real agentic traces — tool outputs, logs, RAG chunks, code. Install with `pip install "headroom-ai[ml]"`.
-- **Cross-agent memory and learning.** Claude Code saves a fact, Codex reads it back. `headroom learn` mines failed sessions and writes corrections straight to `CLAUDE.md` / `AGENTS.md` / `GEMINI.md` — reliability compounds over time.
-- **Reversible (CCR).** Compression is not deletion. The model can always call `headroom_retrieve` to pull the original bytes. Nothing is thrown away.
-
-Bundles the [RTK](https://github.com/rtk-ai/rtk) binary for shell-output rewriting — full [attribution below](#compared-to).
-
----
-
-## How it fits
-
-```
- Your agent / app
- (Claude Code, Cursor, Codex, LangChain, Agno, Strands, your own code…)
- │ prompts · tool outputs · logs · RAG results · files
- ▼
- ┌────────────────────────────────────────────────────┐
- │ Headroom (runs locally — your data stays here) │
- │ ─────────────────────────────────────────────── │
- │ CacheAligner → ContentRouter → CCR │
- │ ├─ SmartCrusher (JSON) │
- │ ├─ CodeCompressor (AST) │
- │ └─ Kompress-base (text, HF) │
- │ │
- │ Cross-agent memory · headroom learn · MCP │
- └────────────────────────────────────────────────────┘
- │ compressed prompt + retrieval tool
- ▼
- LLM provider (Anthropic · OpenAI · Bedrock · …)
-```
-
-→ [Architecture](https://headroom-docs.vercel.app/docs/architecture) · [CCR reversible compression](https://headroom-docs.vercel.app/docs/ccr) · [Kompress-base model card](https://huggingface.co/chopratejas/kompress-base)
-
----
-
-## Proof
-
-**Savings on real agent workloads:**
-
-| Workload | Before | After | Savings |
-|-------------------------------|-------:|-------:|--------:|
-| Code search (100 results) | 17,765 | 1,408 | **92%** |
-| SRE incident debugging | 65,694 | 5,118 | **92%** |
-| GitHub issue triage | 54,174 | 14,761 | **73%** |
-| Codebase exploration | 78,502 | 41,254 | **47%** |
-
-**Accuracy preserved on standard benchmarks:**
-
-| Benchmark | Category | N | Baseline | Headroom | Delta |
-|------------|----------|----:|---------:|---------:|----------:|
-| GSM8K | Math | 100 | 0.870 | 0.870 | **±0.000**|
-| TruthfulQA | Factual | 100 | 0.530 | 0.560 | **+0.030**|
-| SQuAD v2 | QA | 100 | — | **97%** | 19% compression |
-| BFCL | Tools | 100 | — | **97%** | 32% compression |
-
-Reproduce:
-
-```bash
-python -m headroom.evals suite --tier 1
-```
-
-**Community, live:**
-
-
-
-→ [Full benchmarks & methodology](https://headroom-docs.vercel.app/docs/benchmarks)
-
----
-
-## Built for coding agents
-
-| Agent | One-command wrap | Notes |
-|--------------------|------------------------------------|------------------------------------------------------------------|
-| **Claude Code** | `headroom wrap claude` | `--memory` for cross-agent memory, `--code-graph` for codebase intel |
-| **Codex** | `headroom wrap codex --memory` | Shares the same memory store as Claude |
-| **Cursor** | `headroom wrap cursor` | Prints Cursor config — paste once, done |
-| **Aider** | `headroom wrap aider` | Starts proxy, launches Aider |
-| **Copilot CLI** | `headroom wrap copilot` | Starts proxy, launches Copilot |
-| **OpenClaw** | `headroom wrap openclaw` | Installs Headroom as ContextEngine plugin |
-
-MCP-native too — `headroom mcp install` exposes `headroom_compress`, `headroom_retrieve`, and `headroom_stats` to any MCP client.
-
-
-

-
-
----
-
-## Integrations
-
-
-Drop Headroom into any stack
-
-| Your setup | Hook in with |
-|-------------------------|------------------------------------------------------------------|
-| Any Python app | `compress(messages, model=…)` |
-| Any TypeScript app | `await compress(messages, { model })` |
-| Anthropic / OpenAI SDK | `withHeadroom(new Anthropic())` · `withHeadroom(new OpenAI())` |
-| Vercel AI SDK | `wrapLanguageModel({ model, middleware: headroomMiddleware() })` |
-| LiteLLM | `litellm.callbacks = [HeadroomCallback()]` |
-| LangChain | `HeadroomChatModel(your_llm)` |
-| Agno | `HeadroomAgnoModel(your_model)` |
-| Strands | [Strands guide](https://headroom-docs.vercel.app/docs/strands) |
-| ASGI apps | `app.add_middleware(CompressionMiddleware)` |
-| Multi-agent | `SharedContext().put / .get` |
-| MCP clients | `headroom mcp install` |
-
-
-
-
-What's inside
-
-- **SmartCrusher** — universal JSON: arrays of dicts, nested objects, mixed types.
-- **CodeCompressor** — AST-aware for Python, JS, Go, Rust, Java, C++.
-- **Kompress-base** — our HuggingFace model, trained on agentic traces.
-- **Image compression** — 40–90% reduction via trained ML router.
-- **CacheAligner** — stabilizes prefixes so Anthropic/OpenAI KV caches actually hit.
-- **IntelligentContext** — score-based context fitting with learned importance.
-- **CCR** — reversible compression; LLM retrieves originals on demand.
-- **Cross-agent memory** — shared store, agent provenance, auto-dedup.
-- **SharedContext** — compressed context passing across multi-agent workflows.
-- **`headroom learn`** — plugin-based failure mining for Claude, Codex, Gemini.
-
-
-
----
-
-## Install
-
-```bash
-pip install "headroom-ai[all]" # Python, everything
-npm install headroom-ai # TypeScript / Node
-docker pull ghcr.io/chopratejas/headroom:latest
-```
-
-Granular extras: `[proxy]`, `[mcp]`, `[ml]` (Kompress-base), `[agno]`, `[langchain]`, `[evals]`. Requires **Python 3.10+**.
-
-→ [Installation guide](https://headroom-docs.vercel.app/docs/installation) — Docker tags, persistent service, PowerShell, devcontainers.
-
----
-
-## Documentation
-
-| Start here | Go deeper |
-|-------------------------------------------------------------------------|------------------------------------------------------------------------|
-| [Quickstart](https://headroom-docs.vercel.app/docs/quickstart) | [Architecture](https://headroom-docs.vercel.app/docs/architecture) |
-| [Proxy](https://headroom-docs.vercel.app/docs/proxy) | [How compression works](https://headroom-docs.vercel.app/docs/how-compression-works) |
-| [MCP tools](https://headroom-docs.vercel.app/docs/mcp) | [CCR — reversible compression](https://headroom-docs.vercel.app/docs/ccr) |
-| [Memory](https://headroom-docs.vercel.app/docs/memory) | [Cache optimization](https://headroom-docs.vercel.app/docs/cache-optimization) |
-| [Failure learning](https://headroom-docs.vercel.app/docs/failure-learning) | [Benchmarks](https://headroom-docs.vercel.app/docs/benchmarks) |
-| [Configuration](https://headroom-docs.vercel.app/docs/configuration) | [Limitations](https://headroom-docs.vercel.app/docs/limitations) |
-
----
-
-## Compared to
-
-Headroom runs **locally**, covers **every** content type (not just CLI or text), works with every major framework, and is **reversible**.
-
-| | Scope | Deploy | Local | Reversible |
-|----------------------------------|-------------------------------------------------|-------------------------------------|:-----:|:----------:|
-| **Headroom** | All context — tools, RAG, logs, files, history | Proxy · library · middleware · MCP | Yes | Yes |
-| [RTK](https://github.com/rtk-ai/rtk) | CLI command outputs | CLI wrapper | Yes | No |
-| [Compresr](https://compresr.ai), [Token Co.](https://thetokencompany.ai) | Text sent to their API | Hosted API call | No | No |
-| OpenAI Compaction | Conversation history | Provider-native | No | No |
-
-> **Attribution.** Headroom ships with the excellent [RTK](https://github.com/rtk-ai/rtk) binary for shell-output rewriting — `git show` → `git show --short`, noisy `ls` → scoped, chatty installers → summarized. Huge thanks to the RTK team; their tool is a first-class part of our stack, and Headroom compresses everything downstream of it.
-
----
-
-## Contributing
-
-```bash
-git clone https://github.com/chopratejas/headroom.git && cd headroom
-pip install -e ".[dev]" && pytest
-```
-
-Devcontainers in `.devcontainer/` (default + `memory-stack` with Qdrant & Neo4j). See [CONTRIBUTING.md](CONTRIBUTING.md).
-
----
-
-## Community
-
-- **[Live leaderboard](https://headroomlabs.ai/dashboard)** — 60B+ tokens saved and counting.
-- **[Discord](https://discord.gg/yRmaUNpsPJ)** — questions, feedback, war stories.
-- **[Kompress-base on HuggingFace](https://huggingface.co/chopratejas/kompress-base)** — the model behind our text compression.
-
-## License
-
-Apache 2.0 — see [LICENSE](LICENSE).
+
+
+# Headroom
+
+**Compress everything your AI agent reads. Same answers, fraction of the tokens.**
+
+[](https://github.com/chopratejas/headroom/actions/workflows/ci.yml)
+[](https://pypi.org/project/headroom-ai/)
+[](https://www.npmjs.com/package/headroom-ai)
+[](https://huggingface.co/chopratejas/kompress-base)
+[](https://headroomlabs.ai/dashboard)
+[](LICENSE)
+[](https://headroom-docs.vercel.app/docs)
+
+

+
+
+
+---
+
+Every tool call, log line, DB read, RAG chunk, and file your agent injects into a prompt is mostly boilerplate. Headroom strips the noise and keeps the signal — **losslessly, locally, and without touching accuracy.**
+
+> **100 logs. One FATAL error buried at position 67. Both runs found it.**
+> Baseline **10,144 tokens** → Headroom **1,260 tokens** — **87% fewer, identical answer.**
+> `python examples/needle_in_haystack_test.py`
+
+---
+
+## Quick start
+
+Works with Anthropic, OpenAI, Google, Bedrock, Vertex, Azure, OpenRouter, and 100+ models via LiteLLM.
+
+**Wrap your coding agent — one command:**
+
+```bash
+pip install "headroom-ai[all]"
+
+headroom wrap claude # Claude Code
+headroom wrap codex # Codex
+headroom wrap cursor # Cursor
+headroom wrap aider # Aider
+headroom wrap copilot # GitHub Copilot CLI
+```
+
+**Prefer a one-time durable install instead of wrapping every launch:**
+
+```bash
+headroom init -g # Detect installed user-scoped agents and wire them to Headroom
+headroom init claude # Install repo-local Claude hooks for just this project
+headroom init copilot -g # Install user-scoped Copilot hooks and provider routing
+```
+
+**Drop it into your own code — Python or TypeScript:**
+
+```python
+from headroom import compress
+
+result = compress(messages, model="claude-sonnet-4-5")
+response = client.messages.create(model="claude-sonnet-4-5", messages=result.messages)
+print(f"Saved {result.tokens_saved} tokens ({result.compression_ratio:.0%})")
+```
+
+```typescript
+import { compress } from 'headroom-ai';
+const result = await compress(messages, { model: 'gpt-4o' });
+```
+
+**Or run it as a proxy — zero code changes, any language:**
+
+```bash
+headroom proxy --port 8787
+ANTHROPIC_BASE_URL=http://localhost:8787 your-app
+OPENAI_BASE_URL=http://localhost:8787/v1 your-app
+```
+
+---
+
+## Why Headroom
+
+- **Accuracy-preserving.** GSM8K **0.870 → 0.870** (±0.000). TruthfulQA **+0.030**. SQuAD v2 and BFCL both **97%** accuracy after compression. Validated on public OSS benchmarks you can rerun yourself.
+- **Runs on your machine.** No cloud API, no data egress. Compression latency is milliseconds — faster end-to-end for Sonnet / Opus / GPT-4 class models than a hosted service round-trip.
+- **[Kompress-base](https://huggingface.co/chopratejas/kompress-base) on HuggingFace.** Our open-source text compressor, fine-tuned on real agentic traces — tool outputs, logs, RAG chunks, code. Install with `pip install "headroom-ai[ml]"`.
+- **Cross-agent memory and learning.** Claude Code saves a fact, Codex reads it back. `headroom learn` mines failed sessions and writes corrections straight to `CLAUDE.md` / `AGENTS.md` / `GEMINI.md` — reliability compounds over time.
+- **Reversible (CCR).** Compression is not deletion. The model can always call `headroom_retrieve` to pull the original bytes. Nothing is thrown away.
+
+Bundles the [RTK](https://github.com/rtk-ai/rtk) binary for shell-output rewriting — full [attribution below](#compared-to).
+
+---
+
+## How it fits
+
+```
+ Your agent / app
+ (Claude Code, Cursor, Codex, LangChain, Agno, Strands, your own code…)
+ │ prompts · tool outputs · logs · RAG results · files
+ ▼
+ ┌────────────────────────────────────────────────────┐
+ │ Headroom (runs locally — your data stays here) │
+ │ ─────────────────────────────────────────────── │
+ │ CacheAligner → ContentRouter → CCR │
+ │ ├─ SmartCrusher (JSON) │
+ │ ├─ CodeCompressor (AST) │
+ │ └─ Kompress-base (text, HF) │
+ │ │
+ │ Cross-agent memory · headroom learn · MCP │
+ └────────────────────────────────────────────────────┘
+ │ compressed prompt + retrieval tool
+ ▼
+ LLM provider (Anthropic · OpenAI · Bedrock · …)
+```
+
+→ [Architecture](https://headroom-docs.vercel.app/docs/architecture) · [CCR reversible compression](https://headroom-docs.vercel.app/docs/ccr) · [Kompress-base model card](https://huggingface.co/chopratejas/kompress-base)
+
+---
+
+## Proof
+
+**Savings on real agent workloads:**
+
+| Workload | Before | After | Savings |
+|-------------------------------|-------:|-------:|--------:|
+| Code search (100 results) | 17,765 | 1,408 | **92%** |
+| SRE incident debugging | 65,694 | 5,118 | **92%** |
+| GitHub issue triage | 54,174 | 14,761 | **73%** |
+| Codebase exploration | 78,502 | 41,254 | **47%** |
+
+**Accuracy preserved on standard benchmarks:**
+
+| Benchmark | Category | N | Baseline | Headroom | Delta |
+|------------|----------|----:|---------:|---------:|----------:|
+| GSM8K | Math | 100 | 0.870 | 0.870 | **±0.000**|
+| TruthfulQA | Factual | 100 | 0.530 | 0.560 | **+0.030**|
+| SQuAD v2 | QA | 100 | — | **97%** | 19% compression |
+| BFCL | Tools | 100 | — | **97%** | 32% compression |
+
+Reproduce:
+
+```bash
+python -m headroom.evals suite --tier 1
+```
+
+**Community, live:**
+
+
+
+→ [Full benchmarks & methodology](https://headroom-docs.vercel.app/docs/benchmarks)
+
+---
+
+## Built for coding agents
+
+| Agent | Durable init / one-shot wrap | Notes |
+|--------------------|------------------------------------|------------------------------------------------------------------|
+| **Claude Code** | `headroom init claude -g` / `headroom wrap claude` | `init` installs user or repo-local hooks; `wrap` is still useful for ad hoc sessions |
+| **Codex** | `headroom init codex -g` / `headroom wrap codex --memory` | `init` installs provider config plus lifecycle hooks where supported |
+| **Cursor** | `headroom wrap cursor` | Prints Cursor config — durable init not available yet |
+| **Aider** | `headroom wrap aider` | Starts proxy, launches Aider |
+| **Copilot CLI** | `headroom init copilot -g` / `headroom wrap copilot` | `init` installs hooks and BYOK provider routing for the current user |
+| **OpenClaw** | `headroom init openclaw -g` / `headroom wrap openclaw` | Installs Headroom as ContextEngine plugin |
+
+MCP-native too — `headroom mcp install` exposes `headroom_compress`, `headroom_retrieve`, and `headroom_stats` to any MCP client.
+
+
+

+
+
+---
+
+## Integrations
+
+
+Drop Headroom into any stack
+
+| Your setup | Hook in with |
+|-------------------------|------------------------------------------------------------------|
+| Any Python app | `compress(messages, model=…)` |
+| Any TypeScript app | `await compress(messages, { model })` |
+| Anthropic / OpenAI SDK | `withHeadroom(new Anthropic())` · `withHeadroom(new OpenAI())` |
+| Vercel AI SDK | `wrapLanguageModel({ model, middleware: headroomMiddleware() })` |
+| LiteLLM | `litellm.callbacks = [HeadroomCallback()]` |
+| LangChain | `HeadroomChatModel(your_llm)` |
+| Agno | `HeadroomAgnoModel(your_model)` |
+| Strands | [Strands guide](https://headroom-docs.vercel.app/docs/strands) |
+| ASGI apps | `app.add_middleware(CompressionMiddleware)` |
+| Multi-agent | `SharedContext().put / .get` |
+| MCP clients | `headroom mcp install` |
+
+
+
+
+What's inside
+
+- **SmartCrusher** — universal JSON: arrays of dicts, nested objects, mixed types.
+- **CodeCompressor** — AST-aware for Python, JS, Go, Rust, Java, C++.
+- **Kompress-base** — our HuggingFace model, trained on agentic traces.
+- **Image compression** — 40–90% reduction via trained ML router.
+- **CacheAligner** — stabilizes prefixes so Anthropic/OpenAI KV caches actually hit.
+- **IntelligentContext** — score-based context fitting with learned importance.
+- **CCR** — reversible compression; LLM retrieves originals on demand.
+- **Cross-agent memory** — shared store, agent provenance, auto-dedup.
+- **SharedContext** — compressed context passing across multi-agent workflows.
+- **`headroom learn`** — plugin-based failure mining for Claude, Codex, Gemini.
+
+
+
+---
+
+## Install
+
+```bash
+pip install "headroom-ai[all]" # Python, everything
+npm install headroom-ai # TypeScript / Node
+docker pull ghcr.io/chopratejas/headroom:latest
+```
+
+Granular extras: `[proxy]`, `[mcp]`, `[ml]` (Kompress-base), `[agno]`, `[langchain]`, `[evals]`. Requires **Python 3.10+**.
+
+→ [Installation guide](https://headroom-docs.vercel.app/docs/installation) — Docker tags, persistent service, PowerShell, devcontainers.
+
+---
+
+## Documentation
+
+| Start here | Go deeper |
+|-------------------------------------------------------------------------|------------------------------------------------------------------------|
+| [Quickstart](https://headroom-docs.vercel.app/docs/quickstart) | [Architecture](https://headroom-docs.vercel.app/docs/architecture) |
+| [Proxy](https://headroom-docs.vercel.app/docs/proxy) | [How compression works](https://headroom-docs.vercel.app/docs/how-compression-works) |
+| [MCP tools](https://headroom-docs.vercel.app/docs/mcp) | [CCR — reversible compression](https://headroom-docs.vercel.app/docs/ccr) |
+| [Memory](https://headroom-docs.vercel.app/docs/memory) | [Cache optimization](https://headroom-docs.vercel.app/docs/cache-optimization) |
+| [Failure learning](https://headroom-docs.vercel.app/docs/failure-learning) | [Benchmarks](https://headroom-docs.vercel.app/docs/benchmarks) |
+| [Configuration](https://headroom-docs.vercel.app/docs/configuration) | [Limitations](https://headroom-docs.vercel.app/docs/limitations) |
+
+---
+
+## Compared to
+
+Headroom runs **locally**, covers **every** content type (not just CLI or text), works with every major framework, and is **reversible**.
+
+| | Scope | Deploy | Local | Reversible |
+|----------------------------------|-------------------------------------------------|-------------------------------------|:-----:|:----------:|
+| **Headroom** | All context — tools, RAG, logs, files, history | Proxy · library · middleware · MCP | Yes | Yes |
+| [RTK](https://github.com/rtk-ai/rtk) | CLI command outputs | CLI wrapper | Yes | No |
+| [Compresr](https://compresr.ai), [Token Co.](https://thetokencompany.ai) | Text sent to their API | Hosted API call | No | No |
+| OpenAI Compaction | Conversation history | Provider-native | No | No |
+
+> **Attribution.** Headroom ships with the excellent [RTK](https://github.com/rtk-ai/rtk) binary for shell-output rewriting — `git show` → `git show --short`, noisy `ls` → scoped, chatty installers → summarized. Huge thanks to the RTK team; their tool is a first-class part of our stack, and Headroom compresses everything downstream of it.
+
+---
+
+## Contributing
+
+```bash
+git clone https://github.com/chopratejas/headroom.git && cd headroom
+pip install -e ".[dev]" && pytest
+```
+
+Devcontainers in `.devcontainer/` (default + `memory-stack` with Qdrant & Neo4j). See [CONTRIBUTING.md](CONTRIBUTING.md).
+
+---
+
+## Community
+
+- **[Live leaderboard](https://headroomlabs.ai/dashboard)** — 60B+ tokens saved and counting.
+- **[Discord](https://discord.gg/yRmaUNpsPJ)** — questions, feedback, war stories.
+- **[Kompress-base on HuggingFace](https://huggingface.co/chopratejas/kompress-base)** — the model behind our text compression.
+
+## License
+
+Apache 2.0 — see [LICENSE](LICENSE).
diff --git a/headroom/cli/init.py b/headroom/cli/init.py
new file mode 100644
index 000000000..e5ea3ca29
--- /dev/null
+++ b/headroom/cli/init.py
@@ -0,0 +1,679 @@
+"""Durable agent initialization commands."""
+
+from __future__ import annotations
+
+import json
+import os
+import shlex
+import shutil
+import subprocess
+from hashlib import sha1
+from pathlib import Path
+from typing import Any
+
+import click
+
+from headroom.install.models import ConfigScope, InstallPreset, RuntimeKind, SupervisorKind
+from headroom.install.paths import claude_settings_path, codex_config_path, validate_profile_name
+from headroom.install.planner import build_manifest
+from headroom.install.providers import _apply_unix_env_scope, _apply_windows_env_scope
+from headroom.install.runtime import (
+ resolve_headroom_command,
+ start_detached_agent,
+ start_persistent_docker,
+ stop_runtime,
+ wait_ready,
+)
+from headroom.install.state import load_manifest, save_manifest
+from headroom.install.supervisors import start_supervisor
+
+from .main import main
+
+_GLOBAL_PROFILE = "init-user"
+_CLAUDE_HOOK_MARKER = "headroom-init-claude"
+_COPILOT_HOOK_MARKER = "headroom-init-copilot"
+_CODEX_HOOK_MARKER = "headroom-init-codex"
+_CODEX_PROVIDER_MARKER_START = "# --- Headroom init provider ---"
+_CODEX_PROVIDER_MARKER_END = "# --- end Headroom init provider ---"
+_CODEX_FEATURE_MARKER_START = "# --- Headroom init features ---"
+_CODEX_FEATURE_MARKER_END = "# --- end Headroom init features ---"
+_SUPPORTED_TARGETS = ("claude", "copilot", "codex", "openclaw")
+_LOCAL_TARGETS = {"claude", "codex"}
+_GLOBAL_TARGETS = {"claude", "copilot", "codex", "openclaw"}
+
+
+def _command_string(parts: list[str]) -> str:
+ if os.name == "nt":
+ return subprocess.list2cmdline(parts)
+ return shlex.join(parts)
+
+
+def _hook_command(*parts: str) -> str:
+ return _command_string([*resolve_headroom_command(), "init", "hook", "ensure", *parts])
+
+
+def _powershell_matcher() -> str:
+ return "Bash|PowerShell" if os.name == "nt" else "Bash"
+
+
+def _local_profile(cwd: Path | None = None) -> str:
+ root = (cwd or Path.cwd()).resolve()
+ slug = "".join(ch if ch.isalnum() or ch in "-._" else "-" for ch in root.name.lower()).strip(
+ "-"
+ )
+ digest = sha1(str(root).encode("utf-8")).hexdigest()[:8]
+ return validate_profile_name(f"init-{slug or 'repo'}-{digest}")
+
+
+def _runtime_profile(global_scope: bool, cwd: Path | None = None) -> str:
+ return _GLOBAL_PROFILE if global_scope else _local_profile(cwd)
+
+
+def _copilot_config_path() -> Path:
+ return Path.home() / ".copilot" / "config.json"
+
+
+def _codex_hooks_path(global_scope: bool) -> Path:
+ return (Path.home() if global_scope else Path.cwd()) / ".codex" / "hooks.json"
+
+
+def _claude_scope_path(global_scope: bool) -> Path:
+ if global_scope:
+ return claude_settings_path()
+ return Path.cwd() / ".claude" / "settings.local.json"
+
+
+def _codex_scope_path(global_scope: bool) -> Path:
+ if global_scope:
+ return codex_config_path()
+ return Path.cwd() / ".codex" / "config.toml"
+
+
+def _json_file(path: Path) -> dict[str, Any]:
+ if not path.exists():
+ return {}
+ content = path.read_text(encoding="utf-8").strip()
+ if not content:
+ return {}
+ payload = json.loads(content)
+ return payload if isinstance(payload, dict) else {}
+
+
+def _write_json(path: Path, payload: dict[str, Any]) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
+
+
+def _ensure_claude_hooks(path: Path, profile: str, port: int) -> None:
+ payload = _json_file(path)
+ env_map = dict(payload.get("env") or {}) if isinstance(payload.get("env"), dict) else {}
+ env_map["ANTHROPIC_BASE_URL"] = f"http://127.0.0.1:{port}"
+ payload["env"] = env_map
+
+ hooks = dict(payload.get("hooks") or {}) if isinstance(payload.get("hooks"), dict) else {}
+ command = _hook_command("--profile", profile)
+ for event, matcher in (
+ ("SessionStart", "startup|resume"),
+ ("PreToolUse", _powershell_matcher()),
+ ):
+ entries = list(hooks.get(event) or []) if isinstance(hooks.get(event), list) else []
+ retained: list[dict[str, Any]] = []
+ for entry in entries:
+ if not isinstance(entry, dict):
+ retained.append(entry)
+ continue
+ hook_items = entry.get("hooks")
+ if not isinstance(hook_items, list):
+ retained.append(entry)
+ continue
+ has_headroom = any(
+ isinstance(item, dict)
+ and item.get("command")
+ and _CLAUDE_HOOK_MARKER in str(item.get("command"))
+ for item in hook_items
+ )
+ if not has_headroom:
+ retained.append(entry)
+ retained.append(
+ {
+ "matcher": matcher,
+ "hooks": [
+ {
+ "type": "command",
+ "command": f"{command} --marker {_CLAUDE_HOOK_MARKER}",
+ "timeout": 15,
+ }
+ ],
+ }
+ )
+ hooks[event] = retained
+ payload["hooks"] = hooks
+ _write_json(path, payload)
+
+
+def _ensure_copilot_hooks(path: Path, profile: str) -> None:
+ payload = _json_file(path)
+ hooks = dict(payload.get("hooks") or {}) if isinstance(payload.get("hooks"), dict) else {}
+ command = f"{_hook_command('--profile', profile)} --marker {_COPILOT_HOOK_MARKER}"
+ for event in ("SessionStart", "PreToolUse"):
+ entries = list(hooks.get(event) or []) if isinstance(hooks.get(event), list) else []
+ retained = [
+ entry
+ for entry in entries
+ if not (
+ isinstance(entry, dict) and _COPILOT_HOOK_MARKER in str(entry.get("command", ""))
+ )
+ ]
+ retained.append({"type": "command", "command": command, "cwd": ".", "timeout": 15})
+ hooks[event] = retained
+ payload["hooks"] = hooks
+ _write_json(path, payload)
+
+
+def _replace_marker_block(content: str, marker_start: str, marker_end: str, block: str) -> str:
+ if marker_start in content and marker_end in content:
+ start = content.index(marker_start)
+ end = content.index(marker_end) + len(marker_end)
+ content = content[:start].rstrip() + "\n\n" + content[end:].lstrip()
+ return (content.rstrip() + "\n\n" + block.strip() + "\n").lstrip()
+
+
+def _ensure_codex_provider(path: Path, port: int) -> None:
+ block = (
+ f"{_CODEX_PROVIDER_MARKER_START}\n"
+ 'model_provider = "headroom"\n\n'
+ "[model_providers.headroom]\n"
+ 'name = "Headroom init proxy"\n'
+ f'base_url = "http://127.0.0.1:{port}/v1"\n'
+ 'env_key = "OPENAI_API_KEY"\n'
+ "requires_openai_auth = true\n"
+ "supports_websockets = true\n"
+ f"{_CODEX_PROVIDER_MARKER_END}"
+ )
+ content = path.read_text(encoding="utf-8") if path.exists() else ""
+ content = _replace_marker_block(
+ content, _CODEX_PROVIDER_MARKER_START, _CODEX_PROVIDER_MARKER_END, block
+ )
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(content, encoding="utf-8")
+
+
+def _ensure_codex_feature_flag(path: Path) -> None:
+ content = path.read_text(encoding="utf-8") if path.exists() else ""
+ if _CODEX_FEATURE_MARKER_START in content and _CODEX_FEATURE_MARKER_END in content:
+ block = f"{_CODEX_FEATURE_MARKER_START}\ncodex_hooks = true\n{_CODEX_FEATURE_MARKER_END}"
+ content = _replace_marker_block(
+ content,
+ _CODEX_FEATURE_MARKER_START,
+ _CODEX_FEATURE_MARKER_END,
+ block,
+ )
+ elif "[features]" in content:
+ lines = content.splitlines()
+ inserted = False
+ for index, line in enumerate(lines):
+ if line.strip() != "[features]":
+ continue
+ section_end = index + 1
+ while section_end < len(lines) and not (
+ lines[section_end].startswith("[") and lines[section_end].endswith("]")
+ ):
+ if "codex_hooks" in lines[section_end]:
+ inserted = True
+ break
+ section_end += 1
+ if not inserted:
+ lines[index + 1 : index + 1] = [
+ _CODEX_FEATURE_MARKER_START,
+ "codex_hooks = true",
+ _CODEX_FEATURE_MARKER_END,
+ ]
+ inserted = True
+ break
+ content = "\n".join(lines).rstrip() + "\n"
+ if not inserted:
+ content = (
+ content.rstrip()
+ + "\n\n[features]\n"
+ + _CODEX_FEATURE_MARKER_START
+ + "\n"
+ + "codex_hooks = true\n"
+ + _CODEX_FEATURE_MARKER_END
+ + "\n"
+ )
+ else:
+ content = (
+ content.rstrip()
+ + "\n\n[features]\n"
+ + _CODEX_FEATURE_MARKER_START
+ + "\n"
+ + "codex_hooks = true\n"
+ + _CODEX_FEATURE_MARKER_END
+ + "\n"
+ ).lstrip()
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(content, encoding="utf-8")
+
+
+def _ensure_codex_hooks(path: Path, profile: str) -> None:
+ command = f"{_hook_command('--profile', profile)} --marker {_CODEX_HOOK_MARKER}"
+ payload = {
+ "hooks": {
+ "SessionStart": [
+ {
+ "matcher": "startup|resume",
+ "hooks": [{"type": "command", "command": command, "timeout": 15}],
+ }
+ ],
+ "PreToolUse": [
+ {
+ "matcher": "Bash",
+ "hooks": [{"type": "command", "command": command, "timeout": 15}],
+ }
+ ],
+ }
+ }
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
+
+
+def _manifest_changed(
+ existing: Any,
+ *,
+ port: int,
+ backend: str,
+ anyllm_provider: str | None,
+ region: str | None,
+ memory: bool,
+) -> bool:
+ return any(
+ [
+ getattr(existing, "port", port) != port,
+ getattr(existing, "backend", backend) != backend,
+ getattr(existing, "anyllm_provider", anyllm_provider) != anyllm_provider,
+ getattr(existing, "region", region) != region,
+ getattr(existing, "memory_enabled", memory) != memory,
+ ]
+ )
+
+
+def _ensure_runtime_manifest(
+ *,
+ global_scope: bool,
+ targets: list[str],
+ port: int,
+ backend: str,
+ anyllm_provider: str | None,
+ region: str | None,
+ memory: bool,
+) -> str:
+ profile = _runtime_profile(global_scope)
+ existing = load_manifest(profile)
+ merged_targets = sorted(set(existing.targets if existing else []).union(targets))
+ manifest = build_manifest(
+ profile=profile,
+ preset=InstallPreset.PERSISTENT_TASK.value,
+ runtime_kind=RuntimeKind.PYTHON.value,
+ scope=ConfigScope.USER.value,
+ provider_mode="manual",
+ targets=merged_targets,
+ port=port,
+ backend=backend,
+ anyllm_provider=anyllm_provider,
+ region=region,
+ proxy_mode="token",
+ memory_enabled=memory,
+ telemetry_enabled=True,
+ image="ghcr.io/chopratejas/headroom:latest",
+ )
+ manifest.supervisor_kind = SupervisorKind.NONE.value
+ manifest.artifacts = []
+ manifest.mutations = existing.mutations if existing else []
+ if existing is not None and _manifest_changed(
+ existing,
+ port=port,
+ backend=backend,
+ anyllm_provider=anyllm_provider,
+ region=region,
+ memory=memory,
+ ):
+ try:
+ stop_runtime(existing)
+ except Exception:
+ pass
+ save_manifest(manifest)
+ return profile
+
+
+def _env_manifest(values: dict[str, str]) -> Any:
+ return build_manifest(
+ profile="init-env",
+ preset=InstallPreset.PERSISTENT_TASK.value,
+ runtime_kind=RuntimeKind.PYTHON.value,
+ scope=ConfigScope.USER.value,
+ provider_mode="manual",
+ targets=["copilot"],
+ port=8787,
+ backend="anthropic",
+ anyllm_provider=None,
+ region=None,
+ proxy_mode="token",
+ memory_enabled=False,
+ telemetry_enabled=True,
+ image="ghcr.io/chopratejas/headroom:latest",
+ )
+
+
+def _apply_user_env(values: dict[str, str]) -> None:
+ manifest = _env_manifest(values)
+ manifest.base_env = {}
+ manifest.tool_envs = {"copilot": values}
+ if os.name == "nt":
+ _apply_windows_env_scope(manifest)
+ else:
+ _apply_unix_env_scope(manifest)
+
+
+def _resolve_copilot_env(port: int, backend: str) -> dict[str, str]:
+ if backend == "anthropic":
+ return {
+ "COPILOT_PROVIDER_TYPE": "anthropic",
+ "COPILOT_PROVIDER_BASE_URL": f"http://127.0.0.1:{port}",
+ }
+ return {
+ "COPILOT_PROVIDER_TYPE": "openai",
+ "COPILOT_PROVIDER_BASE_URL": f"http://127.0.0.1:{port}/v1",
+ "COPILOT_PROVIDER_WIRE_API": "completions",
+ }
+
+
+def _marketplace_source() -> str:
+ override = os.environ.get("HEADROOM_MARKETPLACE_SOURCE")
+ if override:
+ return override
+ repo_root = Path(__file__).resolve().parents[2]
+ if (repo_root / ".claude-plugin" / "marketplace.json").exists():
+ return str(repo_root)
+ return "JerrettDavis/headroom"
+
+
+def _run_checked(command: list[str], *, action: str) -> None:
+ result = subprocess.run(
+ command,
+ capture_output=True,
+ text=True,
+ encoding="utf-8",
+ errors="replace",
+ )
+ if result.returncode == 0:
+ return
+ detail = "\n".join(part for part in (result.stderr.strip(), result.stdout.strip()) if part)
+ if "already" in detail.lower() or "exists" in detail.lower():
+ return
+ raise click.ClickException(f"{action} failed: {detail or result.returncode}")
+
+
+def _install_claude_marketplace(scope: str) -> None:
+ claude_bin = shutil.which("claude")
+ if not claude_bin:
+ raise click.ClickException("'claude' not found in PATH. Install Claude Code first.")
+ source = _marketplace_source()
+ _run_checked(
+ [claude_bin, "plugin", "marketplace", "add", source], action="claude marketplace add"
+ )
+ _run_checked(
+ [claude_bin, "plugin", "install", "headroom@headroom-marketplace", "--scope", scope],
+ action="claude plugin install",
+ )
+
+
+def _install_copilot_marketplace() -> None:
+ copilot_bin = shutil.which("copilot")
+ if not copilot_bin:
+ raise click.ClickException("'copilot' not found in PATH. Install GitHub Copilot CLI first.")
+ source = _marketplace_source()
+ _run_checked(
+ [copilot_bin, "plugin", "marketplace", "add", source],
+ action="copilot marketplace add",
+ )
+ _run_checked(
+ [copilot_bin, "plugin", "install", "headroom@headroom-marketplace"],
+ action="copilot plugin install",
+ )
+
+
+def _ensure_profile_running(profile: str) -> None:
+ manifest = load_manifest(profile)
+ if manifest is None:
+ return
+ if wait_ready(manifest, timeout_seconds=1):
+ return
+ try:
+ if manifest.preset == InstallPreset.PERSISTENT_DOCKER.value:
+ start_persistent_docker(manifest)
+ elif manifest.supervisor_kind == SupervisorKind.SERVICE.value:
+ start_supervisor(manifest)
+ else:
+ start_detached_agent(manifest.profile)
+ wait_ready(manifest, timeout_seconds=45)
+ except Exception:
+ return
+
+
+def detect_init_targets(global_scope: bool) -> list[str]:
+ allowed = _GLOBAL_TARGETS if global_scope else _LOCAL_TARGETS
+ detected: list[str] = []
+ for target in _SUPPORTED_TARGETS:
+ if target not in allowed:
+ continue
+ if shutil.which(target):
+ detected.append(target)
+ return detected
+
+
+def _init_claude(*, global_scope: bool, profile: str, port: int) -> None:
+ _ensure_claude_hooks(_claude_scope_path(global_scope), profile, port)
+ _install_claude_marketplace("user" if global_scope else "local")
+ click.echo(f"Configured Claude Code ({'user' if global_scope else 'local'} scope).")
+ click.echo("Restart Claude Code to activate Headroom hooks and provider routing.")
+
+
+def _init_copilot(*, global_scope: bool, profile: str, port: int, backend: str) -> None:
+ if not global_scope:
+ raise click.ClickException(
+ "Copilot durable init currently requires -g (current-user scope)."
+ )
+ _ensure_copilot_hooks(_copilot_config_path(), profile)
+ _apply_user_env(_resolve_copilot_env(port, backend))
+ _install_copilot_marketplace()
+ click.echo("Configured GitHub Copilot CLI (user scope).")
+ click.echo("Restart Copilot CLI to activate Headroom hooks and provider routing.")
+
+
+def _init_codex(*, global_scope: bool, profile: str, port: int) -> None:
+ config_path = _codex_scope_path(global_scope)
+ _ensure_codex_provider(config_path, port)
+ _ensure_codex_feature_flag(config_path)
+ _ensure_codex_hooks(_codex_hooks_path(global_scope), profile)
+ click.echo(f"Configured Codex ({'user' if global_scope else 'local'} scope).")
+ if os.name == "nt":
+ click.echo(
+ "Codex hooks are currently disabled upstream on Windows; provider routing was still installed."
+ )
+ click.echo("Restart Codex to activate Headroom configuration.")
+
+
+def _init_openclaw(*, global_scope: bool, port: int) -> None:
+ if not global_scope:
+ raise click.ClickException(
+ "OpenClaw durable init currently requires -g (current-user scope)."
+ )
+ command = [*resolve_headroom_command(), "wrap", "openclaw", "--proxy-port", str(port)]
+ result = subprocess.run(command)
+ if result.returncode != 0:
+ raise SystemExit(result.returncode)
+
+
+def _run_init_targets(
+ *,
+ targets: list[str],
+ global_scope: bool,
+ port: int,
+ backend: str,
+ anyllm_provider: str | None,
+ region: str | None,
+ memory: bool,
+) -> None:
+ runtime_targets = [target for target in targets if target != "openclaw"]
+ profile = _ensure_runtime_manifest(
+ global_scope=global_scope,
+ targets=runtime_targets,
+ port=port,
+ backend=backend,
+ anyllm_provider=anyllm_provider,
+ region=region,
+ memory=memory,
+ )
+ for target in targets:
+ if target == "claude":
+ _init_claude(global_scope=global_scope, profile=profile, port=port)
+ elif target == "copilot":
+ _init_copilot(global_scope=global_scope, profile=profile, port=port, backend=backend)
+ elif target == "codex":
+ _init_codex(global_scope=global_scope, profile=profile, port=port)
+ elif target == "openclaw":
+ _init_openclaw(global_scope=global_scope, port=port)
+
+
+@main.group(invoke_without_command=True)
+@click.option("-g", "--global", "global_scope", is_flag=True, help="Install for the current user.")
+@click.option("--port", default=8787, type=int, show_default=True, help="Headroom proxy port.")
+@click.option("--backend", default="anthropic", show_default=True, help="Proxy backend.")
+@click.option("--anyllm-provider", default=None, help="Provider for any-llm backends.")
+@click.option("--region", default=None, help="Cloud region for Bedrock / Vertex style backends.")
+@click.option("--memory", is_flag=True, help="Enable persistent memory in the proxy runtime.")
+@click.pass_context
+def init(
+ ctx: click.Context,
+ global_scope: bool,
+ port: int,
+ backend: str,
+ anyllm_provider: str | None,
+ region: str | None,
+ memory: bool,
+) -> None:
+ """Install durable Headroom integrations for supported agents."""
+ if ctx.invoked_subcommand is not None:
+ ctx.obj = {
+ "global_scope": global_scope,
+ "port": port,
+ "backend": backend,
+ "anyllm_provider": anyllm_provider,
+ "region": region,
+ "memory": memory,
+ }
+ return
+
+ targets = detect_init_targets(global_scope)
+ if not targets:
+ scope_label = "user" if global_scope else "local"
+ raise click.ClickException(
+ f"No supported {scope_label} init targets were auto-detected. Specify one explicitly."
+ )
+ _run_init_targets(
+ targets=targets,
+ global_scope=global_scope,
+ port=port,
+ backend=backend,
+ anyllm_provider=anyllm_provider,
+ region=region,
+ memory=memory,
+ )
+
+
+def _ctx_value(ctx: click.Context, key: str) -> Any:
+ return (ctx.obj or {}).get(key)
+
+
+@init.command("claude")
+@click.pass_context
+def init_claude(ctx: click.Context) -> None:
+ """Install Claude Code durable hooks and provider routing."""
+ _run_init_targets(
+ targets=["claude"],
+ global_scope=bool(_ctx_value(ctx, "global_scope")),
+ port=int(_ctx_value(ctx, "port") or 8787),
+ backend=str(_ctx_value(ctx, "backend") or "anthropic"),
+ anyllm_provider=_ctx_value(ctx, "anyllm_provider"),
+ region=_ctx_value(ctx, "region"),
+ memory=bool(_ctx_value(ctx, "memory")),
+ )
+
+
+@init.command("copilot")
+@click.pass_context
+def init_copilot(ctx: click.Context) -> None:
+ """Install GitHub Copilot CLI durable hooks and provider routing."""
+ _run_init_targets(
+ targets=["copilot"],
+ global_scope=bool(_ctx_value(ctx, "global_scope")),
+ port=int(_ctx_value(ctx, "port") or 8787),
+ backend=str(_ctx_value(ctx, "backend") or "anthropic"),
+ anyllm_provider=_ctx_value(ctx, "anyllm_provider"),
+ region=_ctx_value(ctx, "region"),
+ memory=bool(_ctx_value(ctx, "memory")),
+ )
+
+
+@init.command("codex")
+@click.pass_context
+def init_codex(ctx: click.Context) -> None:
+ """Install Codex durable hooks and provider routing."""
+ _run_init_targets(
+ targets=["codex"],
+ global_scope=bool(_ctx_value(ctx, "global_scope")),
+ port=int(_ctx_value(ctx, "port") or 8787),
+ backend=str(_ctx_value(ctx, "backend") or "anthropic"),
+ anyllm_provider=_ctx_value(ctx, "anyllm_provider"),
+ region=_ctx_value(ctx, "region"),
+ memory=bool(_ctx_value(ctx, "memory")),
+ )
+
+
+@init.command("openclaw")
+@click.pass_context
+def init_openclaw(ctx: click.Context) -> None:
+ """Install the durable OpenClaw Headroom plugin."""
+ _run_init_targets(
+ targets=["openclaw"],
+ global_scope=bool(_ctx_value(ctx, "global_scope")),
+ port=int(_ctx_value(ctx, "port") or 8787),
+ backend=str(_ctx_value(ctx, "backend") or "anthropic"),
+ anyllm_provider=_ctx_value(ctx, "anyllm_provider"),
+ region=_ctx_value(ctx, "region"),
+ memory=bool(_ctx_value(ctx, "memory")),
+ )
+
+
+@init.group("hook", hidden=True)
+def init_hook() -> None:
+ """Internal hook helpers."""
+
+
+@init_hook.command("ensure")
+@click.option("--profile", default=None, help="Explicit deployment profile to ensure.")
+@click.option("--marker", default=None, hidden=True)
+def init_hook_ensure(profile: str | None, marker: str | None) -> None:
+ """Best-effort ensure used by installed agent hooks."""
+ del marker
+ profiles: list[str] = []
+ if profile:
+ profiles.append(profile)
+ else:
+ local_profile = _local_profile()
+ if load_manifest(local_profile) is not None:
+ profiles.append(local_profile)
+ elif load_manifest(_GLOBAL_PROFILE) is not None:
+ profiles.append(_GLOBAL_PROFILE)
+ for name in profiles:
+ _ensure_profile_running(name)
diff --git a/headroom/cli/main.py b/headroom/cli/main.py
index c0bfaabf4..3d8eef75b 100644
--- a/headroom/cli/main.py
+++ b/headroom/cli/main.py
@@ -1,78 +1,79 @@
-"""Main CLI entry point for Headroom."""
-
-import click
-
-CLI_CONTEXT_SETTINGS = {"help_option_names": ["--help", "-?"]}
-
-
-def get_version() -> str:
- """Get the current version."""
- try:
- from headroom._version import __version__
-
- return __version__
- except ImportError:
- return "unknown"
-
-
-@click.group(context_settings=CLI_CONTEXT_SETTINGS)
-@click.version_option(get_version(), "--version", "-v", prog_name="headroom")
-@click.pass_context
-def main(ctx: click.Context) -> None:
- """Headroom - The Context Optimization Layer for LLM Applications.
-
- Manage memories, run the optimization proxy, and analyze metrics.
-
- \b
- Examples:
- headroom proxy Start the optimization proxy
- headroom memory list List stored memories
- headroom memory stats Show memory statistics
- """
- ctx.ensure_object(dict)
-
-
-# Import subcommands - these register themselves with the main group
-def _register_commands() -> None:
- """Register all subcommand groups."""
- from . import (
- evals, # noqa: F401
- install, # noqa: F401
- learn, # noqa: F401
- mcp, # noqa: F401
- perf, # noqa: F401
- proxy, # noqa: F401
- tools, # noqa: F401
- wrap, # noqa: F401
- )
-
- # Memory CLI requires numpy/hnswlib — optional
- try:
- from . import memory # noqa: F401
- except ImportError:
- pass
-
-
-_register_commands()
-
-
-def _apply_help_aliases(command: click.Command) -> None:
- """Ensure `-?` works everywhere in the Click command tree."""
- context_settings = dict(command.context_settings or {})
- help_option_names = list(context_settings.get("help_option_names", []))
- if "--help" not in help_option_names:
- help_option_names.append("--help")
- if "-?" not in help_option_names:
- help_option_names.append("-?")
- context_settings["help_option_names"] = help_option_names
- command.context_settings = context_settings
-
- if isinstance(command, click.Group):
- for child in command.commands.values():
- _apply_help_aliases(child)
-
-
-_apply_help_aliases(main)
-
-if __name__ == "__main__":
- main()
+"""Main CLI entry point for Headroom."""
+
+import click
+
+CLI_CONTEXT_SETTINGS = {"help_option_names": ["--help", "-?"]}
+
+
+def get_version() -> str:
+ """Get the current version."""
+ try:
+ from headroom._version import __version__
+
+ return __version__
+ except ImportError:
+ return "unknown"
+
+
+@click.group(context_settings=CLI_CONTEXT_SETTINGS)
+@click.version_option(get_version(), "--version", "-v", prog_name="headroom")
+@click.pass_context
+def main(ctx: click.Context) -> None:
+ """Headroom - The Context Optimization Layer for LLM Applications.
+
+ Manage memories, run the optimization proxy, and analyze metrics.
+
+ \b
+ Examples:
+ headroom proxy Start the optimization proxy
+ headroom memory list List stored memories
+ headroom memory stats Show memory statistics
+ """
+ ctx.ensure_object(dict)
+
+
+# Import subcommands - these register themselves with the main group
+def _register_commands() -> None:
+ """Register all subcommand groups."""
+ from . import (
+ evals, # noqa: F401
+ init, # noqa: F401
+ install, # noqa: F401
+ learn, # noqa: F401
+ mcp, # noqa: F401
+ perf, # noqa: F401
+ proxy, # noqa: F401
+ tools, # noqa: F401
+ wrap, # noqa: F401
+ )
+
+ # Memory CLI requires numpy/hnswlib — optional
+ try:
+ from . import memory # noqa: F401
+ except ImportError:
+ pass
+
+
+_register_commands()
+
+
+def _apply_help_aliases(command: click.Command) -> None:
+ """Ensure `-?` works everywhere in the Click command tree."""
+ context_settings = dict(command.context_settings or {})
+ help_option_names = list(context_settings.get("help_option_names", []))
+ if "--help" not in help_option_names:
+ help_option_names.append("--help")
+ if "-?" not in help_option_names:
+ help_option_names.append("-?")
+ context_settings["help_option_names"] = help_option_names
+ command.context_settings = context_settings
+
+ if isinstance(command, click.Group):
+ for child in command.commands.values():
+ _apply_help_aliases(child)
+
+
+_apply_help_aliases(main)
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/headroom-agent-hooks/.claude-plugin/plugin.json b/plugins/headroom-agent-hooks/.claude-plugin/plugin.json
new file mode 100644
index 000000000..6d180897c
--- /dev/null
+++ b/plugins/headroom-agent-hooks/.claude-plugin/plugin.json
@@ -0,0 +1,17 @@
+{
+ "name": "headroom",
+ "version": "0.1.0",
+ "description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
+ "author": {
+ "name": "Headroom Contributors",
+ "url": "https://github.com/JerrettDavis/headroom"
+ },
+ "homepage": "https://github.com/JerrettDavis/headroom",
+ "repository": "https://github.com/JerrettDavis/headroom",
+ "keywords": [
+ "headroom",
+ "hooks",
+ "claude-code",
+ "copilot-cli"
+ ]
+}
diff --git a/plugins/headroom-agent-hooks/.github/plugin/plugin.json b/plugins/headroom-agent-hooks/.github/plugin/plugin.json
new file mode 100644
index 000000000..143337063
--- /dev/null
+++ b/plugins/headroom-agent-hooks/.github/plugin/plugin.json
@@ -0,0 +1,18 @@
+{
+ "name": "headroom",
+ "version": "0.1.0",
+ "description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
+ "author": {
+ "name": "Headroom Contributors",
+ "url": "https://github.com/JerrettDavis/headroom"
+ },
+ "homepage": "https://github.com/JerrettDavis/headroom",
+ "repository": "https://github.com/JerrettDavis/headroom",
+ "keywords": [
+ "headroom",
+ "hooks",
+ "claude-code",
+ "copilot-cli"
+ ],
+ "hooks": "./hooks"
+}
diff --git a/plugins/headroom-agent-hooks/README.md b/plugins/headroom-agent-hooks/README.md
new file mode 100644
index 000000000..fc78f1d9e
--- /dev/null
+++ b/plugins/headroom-agent-hooks/README.md
@@ -0,0 +1,11 @@
+# Headroom agent hooks
+
+This plugin exposes lightweight startup hooks for Claude Code and GitHub Copilot CLI.
+
+The hooks call:
+
+```bash
+headroom init hook ensure
+```
+
+That hidden helper checks for a matching durable `headroom init` deployment and starts it if needed.
diff --git a/plugins/headroom-agent-hooks/hooks/hooks.json b/plugins/headroom-agent-hooks/hooks/hooks.json
new file mode 100644
index 000000000..bf14a3132
--- /dev/null
+++ b/plugins/headroom-agent-hooks/hooks/hooks.json
@@ -0,0 +1,29 @@
+{
+ "description": "Headroom plugin hooks — ensure the local Headroom runtime is available for initialized agents.",
+ "hooks": {
+ "SessionStart": [
+ {
+ "matcher": "startup|resume",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "headroom init hook ensure",
+ "timeout": 15
+ }
+ ]
+ }
+ ],
+ "PreToolUse": [
+ {
+ "matcher": "Bash|PowerShell",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "headroom init hook ensure",
+ "timeout": 15
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/tests/test_cli/test_init_cli.py b/tests/test_cli/test_init_cli.py
new file mode 100644
index 000000000..44af1c5dc
--- /dev/null
+++ b/tests/test_cli/test_init_cli.py
@@ -0,0 +1,202 @@
+from __future__ import annotations
+
+import importlib
+import json
+import sys
+import types
+from pathlib import Path
+
+import click
+from click.testing import CliRunner
+
+
+def _load_init_module(monkeypatch):
+ monkeypatch.delitem(sys.modules, "headroom.cli.init", raising=False)
+ monkeypatch.delitem(sys.modules, "headroom.cli.main", raising=False)
+ fake_main_module = types.ModuleType("headroom.cli.main")
+
+ @click.group()
+ def fake_main() -> None:
+ pass
+
+ fake_main_module.main = fake_main
+ monkeypatch.setitem(sys.modules, "headroom.cli.main", fake_main_module)
+ importlib.invalidate_caches()
+ init_cli = importlib.import_module("headroom.cli.init")
+ monkeypatch.delitem(sys.modules, "headroom.cli.init", raising=False)
+ return init_cli, fake_main
+
+
+def test_init_auto_detects_targets(monkeypatch) -> None:
+ init_cli, fake_main = _load_init_module(monkeypatch)
+ runner = CliRunner()
+ captured: dict[str, object] = {}
+
+ monkeypatch.setattr(init_cli, "detect_init_targets", lambda global_scope: ["claude", "codex"])
+ monkeypatch.setattr(init_cli, "_run_init_targets", lambda **kwargs: captured.update(kwargs))
+
+ result = runner.invoke(fake_main, ["init", "-g"])
+
+ assert result.exit_code == 0, result.output
+ assert captured["targets"] == ["claude", "codex"]
+ assert captured["global_scope"] is True
+
+
+def test_init_fails_when_auto_detection_empty(monkeypatch) -> None:
+ init_cli, fake_main = _load_init_module(monkeypatch)
+ runner = CliRunner()
+ monkeypatch.setattr(init_cli, "detect_init_targets", lambda global_scope: [])
+
+ result = runner.invoke(fake_main, ["init"])
+
+ assert result.exit_code != 0
+ assert "auto-detected" in result.output
+
+
+def test_init_copilot_requires_global(monkeypatch) -> None:
+ init_cli, fake_main = _load_init_module(monkeypatch)
+ runner = CliRunner()
+ monkeypatch.setattr(init_cli, "_ensure_runtime_manifest", lambda **kwargs: "init-local-test")
+
+ result = runner.invoke(fake_main, ["init", "copilot"])
+
+ assert result.exit_code != 0
+ assert "requires -g" in result.output
+
+
+def test_init_claude_local_writes_settings_and_installs_marketplace(
+ monkeypatch, tmp_path: Path
+) -> None:
+ init_cli, fake_main = _load_init_module(monkeypatch)
+ runner = CliRunner()
+ monkeypatch.chdir(tmp_path)
+ marketplace_calls: list[str] = []
+ monkeypatch.setattr(init_cli, "_ensure_runtime_manifest", lambda **kwargs: "init-local-demo")
+ monkeypatch.setattr(
+ init_cli,
+ "_install_claude_marketplace",
+ lambda scope: marketplace_calls.append(scope),
+ )
+
+ result = runner.invoke(fake_main, ["init", "claude"])
+
+ assert result.exit_code == 0, result.output
+ settings_path = tmp_path / ".claude" / "settings.local.json"
+ payload = json.loads(settings_path.read_text(encoding="utf-8"))
+ assert payload["env"]["ANTHROPIC_BASE_URL"] == "http://127.0.0.1:8787"
+ assert marketplace_calls == ["local"]
+ assert any(
+ "--profile init-local-demo" in hook["command"] and "init hook ensure" in hook["command"]
+ for entry in payload["hooks"]["SessionStart"]
+ for hook in entry["hooks"]
+ )
+
+
+def test_init_codex_merges_feature_flag_into_existing_table(monkeypatch, tmp_path: Path) -> None:
+ init_cli, _ = _load_init_module(monkeypatch)
+ monkeypatch.chdir(tmp_path)
+ config_path = tmp_path / ".codex" / "config.toml"
+ config_path.parent.mkdir(parents=True)
+ config_path.write_text("[features]\nshell_tool = true\n", encoding="utf-8")
+
+ init_cli._init_codex(global_scope=False, profile="init-local-demo", port=9000)
+
+ content = config_path.read_text(encoding="utf-8")
+ assert 'base_url = "http://127.0.0.1:9000/v1"' in content
+ assert content.count("[features]") == 1
+ assert "codex_hooks = true" in content
+ hooks = json.loads((tmp_path / ".codex" / "hooks.json").read_text(encoding="utf-8"))
+ assert "--profile init-local-demo" in hooks["hooks"]["SessionStart"][0]["hooks"][0]["command"]
+ assert "init hook ensure" in hooks["hooks"]["SessionStart"][0]["hooks"][0]["command"]
+
+
+def test_init_claude_uses_custom_port(monkeypatch, tmp_path: Path) -> None:
+ init_cli, _ = _load_init_module(monkeypatch)
+ monkeypatch.chdir(tmp_path)
+ monkeypatch.setattr(init_cli, "_install_claude_marketplace", lambda scope: None)
+
+ init_cli._init_claude(global_scope=False, profile="init-local-demo", port=9011)
+
+ payload = json.loads((tmp_path / ".claude" / "settings.local.json").read_text(encoding="utf-8"))
+ assert payload["env"]["ANTHROPIC_BASE_URL"] == "http://127.0.0.1:9011"
+
+
+def test_init_copilot_global_writes_hooks_and_env(monkeypatch, tmp_path: Path) -> None:
+ init_cli, _ = _load_init_module(monkeypatch)
+ captured_env: dict[str, str] = {}
+ monkeypatch.setattr(init_cli, "_copilot_config_path", lambda: tmp_path / "copilot-config.json")
+ monkeypatch.setattr(init_cli, "_apply_user_env", lambda values: captured_env.update(values))
+ monkeypatch.setattr(init_cli, "_install_copilot_marketplace", lambda: None)
+
+ init_cli._init_copilot(global_scope=True, profile="init-user", port=9005, backend="openai")
+
+ payload = json.loads((tmp_path / "copilot-config.json").read_text(encoding="utf-8"))
+ assert "SessionStart" in payload["hooks"]
+ assert "PreToolUse" in payload["hooks"]
+ assert "--profile init-user" in payload["hooks"]["SessionStart"][0]["command"]
+ assert captured_env == {
+ "COPILOT_PROVIDER_TYPE": "openai",
+ "COPILOT_PROVIDER_BASE_URL": "http://127.0.0.1:9005/v1",
+ "COPILOT_PROVIDER_WIRE_API": "completions",
+ }
+
+
+def test_init_hook_ensure_prefers_local_profile(monkeypatch) -> None:
+ init_cli, fake_main = _load_init_module(monkeypatch)
+ ensured: list[str] = []
+
+ def fake_load(profile: str):
+ return object() if profile == "init-repo-12345678" else None
+
+ monkeypatch.setattr(init_cli, "_local_profile", lambda cwd=None: "init-repo-12345678")
+ monkeypatch.setattr(init_cli, "load_manifest", fake_load)
+ monkeypatch.setattr(
+ init_cli, "_ensure_profile_running", lambda profile: ensured.append(profile)
+ )
+
+ runner = CliRunner()
+ result = runner.invoke(fake_main, ["init", "hook", "ensure"])
+
+ assert result.exit_code == 0, result.output
+ assert ensured == ["init-repo-12345678"]
+
+
+def test_init_openclaw_requires_global(monkeypatch) -> None:
+ _, fake_main = _load_init_module(monkeypatch)
+ runner = CliRunner()
+
+ result = runner.invoke(fake_main, ["init", "openclaw"])
+
+ assert result.exit_code != 0
+ assert "requires -g" in result.output
+
+
+def test_init_openclaw_delegates_to_wrap(monkeypatch) -> None:
+ init_cli, _ = _load_init_module(monkeypatch)
+ calls: list[list[str]] = []
+
+ class _Result:
+ returncode = 0
+
+ monkeypatch.setattr(init_cli, "resolve_headroom_command", lambda: ["headroom"])
+ monkeypatch.setattr(
+ init_cli.subprocess,
+ "run",
+ lambda cmd: calls.append(cmd) or _Result(),
+ )
+
+ init_cli._init_openclaw(global_scope=True, port=9999)
+
+ assert calls == [["headroom", "wrap", "openclaw", "--proxy-port", "9999"]]
+
+
+def test_detect_init_targets_respects_scope(monkeypatch) -> None:
+ init_cli, _ = _load_init_module(monkeypatch)
+ monkeypatch.setattr(
+ init_cli.shutil,
+ "which",
+ lambda name: name if name in {"claude", "copilot", "codex", "openclaw"} else None,
+ )
+
+ assert init_cli.detect_init_targets(False) == ["claude", "codex"]
+ assert init_cli.detect_init_targets(True) == ["claude", "copilot", "codex", "openclaw"]
diff --git a/tests/test_plugin_manifests.py b/tests/test_plugin_manifests.py
new file mode 100644
index 000000000..a3cd7b083
--- /dev/null
+++ b/tests/test_plugin_manifests.py
@@ -0,0 +1,40 @@
+from __future__ import annotations
+
+import json
+from pathlib import Path
+
+REPO_ROOT = Path(__file__).resolve().parents[1]
+
+
+def _load_json(relative_path: str) -> object:
+ return json.loads((REPO_ROOT / relative_path).read_text(encoding="utf-8"))
+
+
+def test_marketplace_manifests_match() -> None:
+ assert _load_json(".claude-plugin/marketplace.json") == _load_json(
+ ".github/plugin/marketplace.json"
+ )
+
+
+def test_plugin_manifests_share_core_metadata() -> None:
+ claude = _load_json("plugins/headroom-agent-hooks/.claude-plugin/plugin.json")
+ copilot = _load_json("plugins/headroom-agent-hooks/.github/plugin/plugin.json")
+ assert isinstance(claude, dict)
+ assert isinstance(copilot, dict)
+ for key in ("name", "version", "description", "author", "homepage", "repository", "keywords"):
+ assert claude[key] == copilot[key]
+ assert "hooks" not in claude
+ assert copilot["hooks"] == "./hooks"
+
+
+def test_marketplace_entry_points_to_plugin_root() -> None:
+ marketplace = _load_json(".claude-plugin/marketplace.json")
+ assert isinstance(marketplace, dict)
+ plugins = marketplace["plugins"]
+ assert isinstance(plugins, list)
+ plugin = plugins[0]
+ assert plugin["name"] == "headroom"
+ plugin_root = (REPO_ROOT / plugin["source"]).resolve()
+ assert plugin_root.is_dir()
+ assert (plugin_root / ".claude-plugin" / "plugin.json").is_file()
+ assert (plugin_root / "hooks" / "hooks.json").is_file()