diff --git a/README.md b/README.md
index 6d0ff444..0b683253 100644
--- a/README.md
+++ b/README.md
@@ -132,6 +132,7 @@ Pick your agent. One command. Done.
| **Cursor** | `npx skills add JuliusBrussee/caveman -a cursor` |
| **Windsurf** | `npx skills add JuliusBrussee/caveman -a windsurf` |
| **Copilot** | `npx skills add JuliusBrussee/caveman -a github-copilot` |
+| **opencode** | Clone repo → add `.opencode-plugin/` path to `opencode.json` `plugin` array |
| **Cline** | `npx skills add JuliusBrussee/caveman -a cline` |
| **Any other** | `npx skills add JuliusBrussee/caveman` |
@@ -141,17 +142,17 @@ Install once. Use in every session for that install target after that. One rock.
Auto-activation is built in for Claude Code, Gemini CLI, and the repo-local Codex setup below. `npx skills add` installs the skill for other agents, but does **not** install repo rule/instruction files, so Caveman does not auto-start there unless you add the always-on snippet below.
-| Feature | Claude Code | Codex | Gemini CLI | Cursor | Windsurf | Cline | Copilot |
-|---------|:-----------:|:-----:|:----------:|:------:|:--------:|:-----:|:-------:|
-| Caveman mode | Y | Y | Y | Y | Y | Y | Y |
-| Auto-activate every session | Y | Y¹ | Y | —² | —² | —² | —² |
-| `/caveman` command | Y | Y¹ | Y | — | — | — | — |
-| Mode switching (lite/full/ultra) | Y | Y¹ | Y | Y³ | Y³ | — | — |
-| Statusline badge | Y⁴ | — | — | — | — | — | — |
-| caveman-commit | Y | — | Y | Y | Y | Y | Y |
-| caveman-review | Y | — | Y | Y | Y | Y | Y |
-| caveman-compress | Y | Y | Y | Y | Y | Y | Y |
-| caveman-help | Y | — | Y | Y | Y | Y | Y |
+| Feature | Claude Code | Codex | Gemini CLI | Cursor | Windsurf | Cline | Copilot | opencode |
+|---------|:-----------:|:-----:|:----------:|:------:|:--------:|:-----:|:-------:|:--------:|
+| Caveman mode | Y | Y | Y | Y | Y | Y | Y | Y |
+| Auto-activate every session | Y | Y¹ | Y | —² | —² | —² | —² | Y⁵ |
+| `/caveman` command | Y | Y¹ | Y | — | — | — | — | Y⁵ |
+| Mode switching (lite/full/ultra) | Y | Y¹ | Y | Y³ | Y³ | — | — | Y⁵ |
+| Statusline badge | Y⁴ | — | — | — | — | — | — | — |
+| caveman-commit | Y | — | Y | Y | Y | Y | Y | Y |
+| caveman-review | Y | — | Y | Y | Y | Y | Y | Y |
+| caveman-compress | Y | Y | Y | Y | Y | Y | Y | — |
+| caveman-help | Y | — | Y | Y | Y | Y | Y | — |
> [!NOTE]
> Auto-activation works differently per agent: Claude Code uses SessionStart hooks, this repo's Codex dogfood setup uses `.codex/hooks.json`, Gemini uses context files. Cursor/Windsurf/Cline/Copilot can be made always-on, but `npx skills add` installs only the skill, not the repo rule/instruction files.
@@ -160,6 +161,7 @@ Auto-activation is built in for Claude Code, Gemini CLI, and the repo-local Code
> ² Add the "Want it always on?" snippet below to those agents' system prompt or rule file if you want session-start activation.
> ³ Cursor and Windsurf receive the full SKILL.md with all intensity levels. Mode switching works on-demand via the skill; no slash command.
> ⁴ Available in Claude Code, but plugin install only nudges setup. Standalone `install.sh` / `install.ps1` configures it automatically when no custom `statusLine` exists.
+> ⁵ Via the native opencode plugin (server + TUI). Enable/disable/level commands work per-session. Subagent sessions auto-use ultra.
Claude Code — full details
@@ -248,7 +250,45 @@ Copilot works with Chat, Edits, and Coding Agent.
-Any other agent (opencode, Roo, Amp, Goose, Kiro, and 40+ more)
+opencode — full details
+
+Clone the repo, then add the plugin paths to your opencode config:
+
+```bash
+git clone https://github.com/JuliusBrussee/caveman
+```
+
+Add to `~/.config/opencode/opencode.json`:
+
+```json
+{
+ "plugin": [
+ "/path/to/caveman/plugins/caveman/.opencode-plugin/server.ts"
+ ]
+}
+```
+
+Add to `~/.config/opencode/tui.json`:
+
+```json
+{
+ "plugin": [
+ "/path/to/caveman/plugins/caveman/.opencode-plugin/tui.ts"
+ ]
+}
+```
+
+Auto-activates every session (full mode by default). Subagent sessions auto-use ultra.
+
+TUI commands (via `/` command palette):
+- `/enable_caveman` — enable for current session
+- `/disable_caveman` — disable for current session
+- `/caveman_level` — switch lite / full / ultra
+
+
+
+
+Any other agent (Roo, Amp, Goose, Kiro, and 40+ more)
[npx skills](https://github.com/vercel-labs/skills) supports 40+ agents:
@@ -282,7 +322,6 @@ Code/commits/PRs: normal. Off: "stop caveman" / "normal mode".
Where to put it:
| Agent | File |
|-------|------|
-| opencode | `.config/opencode/AGENTS.md` |
| Roo | `.roo/rules/caveman.md` |
| Amp | your workspace system prompt |
| Others | your agent's system prompt or rules file |
diff --git a/plugins/caveman/.opencode-plugin/package.json b/plugins/caveman/.opencode-plugin/package.json
new file mode 100644
index 00000000..c5d2236d
--- /dev/null
+++ b/plugins/caveman/.opencode-plugin/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "caveman-opencode",
+ "version": "0.1.0",
+ "description": "Caveman mode for opencode — cut filler, keep technical accuracy.",
+ "author": {
+ "name": "Julius Brussee",
+ "url": "https://github.com/JuliusBrussee"
+ },
+ "homepage": "https://github.com/JuliusBrussee/caveman",
+ "repository": "https://github.com/JuliusBrussee/caveman",
+ "license": "MIT",
+ "keywords": [
+ "productivity",
+ "caveman",
+ "brevity"
+ ],
+ "exports": {
+ "./server": "./server.ts",
+ "./tui": "./tui.ts"
+ },
+ "main": "./server.ts"
+}
diff --git a/plugins/caveman/.opencode-plugin/server.ts b/plugins/caveman/.opencode-plugin/server.ts
new file mode 100644
index 00000000..a6751e4b
--- /dev/null
+++ b/plugins/caveman/.opencode-plugin/server.ts
@@ -0,0 +1,68 @@
+import type { Hooks, PluginInput } from "@opencode-ai/plugin"
+import path from "path"
+import fs from "fs/promises"
+import os from "os"
+
+const STATE_FILE = path.join(
+ process.env.XDG_STATE_HOME ?? path.join(os.homedir(), ".local", "state"),
+ "opencode",
+ "caveman.json",
+)
+
+type Level = "lite" | "full" | "ultra"
+type State = { disabled: string[]; levels: Record }
+
+async function readState(): Promise {
+ try {
+ return JSON.parse(await fs.readFile(STATE_FILE, "utf8"))
+ } catch {
+ return { disabled: [], levels: {} }
+ }
+}
+
+const RULES: Record = {
+ lite: `[CAVEMAN MODE: lite] Respond terse. No filler/hedging. Keep articles + full sentences. Professional but tight.
+Drop: filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging.
+Technical terms exact. Code blocks unchanged. Errors quoted exact.
+Auto-clarity: full language for security warnings + irreversible ops.`,
+
+ full: `[CAVEMAN MODE: full] Respond terse like smart caveman. All technical substance stay. Only fluff die.
+Drop: articles (a/an/the), filler, pleasantries, hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for").
+Pattern: \`[thing] [action] [reason]. [next step].\`
+Technical terms exact. Code blocks unchanged. Errors quoted exact.
+Auto-clarity: full language for security warnings + irreversible ops.`,
+
+ ultra: `[CAVEMAN MODE: ultra] Respond ultra-terse. Max compression. All technical substance preserved.
+Abbreviate (DB/auth/config/req/res/fn/impl). Strip articles+conjunctions+filler+hedging+pleasantries. Arrows for causality (X → Y). One word when sufficient. Fragments OK.
+Technical terms exact. Code blocks unchanged. Errors quoted exact.
+Auto-clarity: full language for security warnings + irreversible ops only.`,
+}
+
+// Track subagent sessions — auto-apply ultra for them
+const childSessions = new Set()
+
+async function server(_input: PluginInput): Promise {
+ return {
+ event: async ({ event }: any) => {
+ if (event?.type === "session.created") {
+ const info = event?.properties?.info
+ if (info?.parentID) childSessions.add(info.id)
+ }
+ if (event?.type === "session.deleted") {
+ const sid = event?.properties?.sessionID
+ if (sid) childSessions.delete(sid)
+ }
+ },
+
+ "experimental.chat.system.transform": async (input, output) => {
+ const sid = input.sessionID
+ if (!sid) return
+ const state = await readState()
+ if (state.disabled.includes(sid)) return
+ const level: Level = childSessions.has(sid) ? "ultra" : (state.levels[sid] ?? "full")
+ output.system.push(RULES[level])
+ },
+ }
+}
+
+export default { id: "caveman", server }
diff --git a/plugins/caveman/.opencode-plugin/tui.ts b/plugins/caveman/.opencode-plugin/tui.ts
new file mode 100644
index 00000000..7082636b
--- /dev/null
+++ b/plugins/caveman/.opencode-plugin/tui.ts
@@ -0,0 +1,93 @@
+import type { TuiPlugin } from "@opencode-ai/plugin/tui"
+import path from "path"
+import fs from "fs/promises"
+import os from "os"
+
+const STATE_FILE = path.join(
+ process.env.XDG_STATE_HOME ?? path.join(os.homedir(), ".local", "state"),
+ "opencode",
+ "caveman.json",
+)
+
+type Level = "lite" | "full" | "ultra"
+type State = { disabled: string[]; levels: Record }
+
+async function readState(): Promise {
+ try {
+ return JSON.parse(await fs.readFile(STATE_FILE, "utf8"))
+ } catch {
+ return { disabled: [], levels: {} }
+ }
+}
+
+async function writeState(state: State) {
+ await fs.mkdir(path.dirname(STATE_FILE), { recursive: true })
+ await fs.writeFile(STATE_FILE, JSON.stringify(state), "utf8")
+}
+
+const tui: TuiPlugin = async (api) => {
+ function sid(): string | undefined {
+ const r = api.route.current
+ return r.name === "session" ? (r as any).params?.sessionID : undefined
+ }
+
+ api.command.register(() => [
+ {
+ title: "Enable caveman mode",
+ value: "caveman.enable",
+ category: "Caveman",
+ slash: { name: "enable_caveman" },
+ onSelect: async () => {
+ const id = sid()
+ if (!id) return
+ const state = await readState()
+ state.disabled = state.disabled.filter((x) => x !== id)
+ await writeState(state)
+ api.ui.toast({ variant: "success", message: "Caveman enabled (full mode)" })
+ },
+ },
+ {
+ title: "Disable caveman mode",
+ value: "caveman.disable",
+ category: "Caveman",
+ slash: { name: "disable_caveman" },
+ onSelect: async () => {
+ const id = sid()
+ if (!id) return
+ const state = await readState()
+ if (!state.disabled.includes(id)) state.disabled.push(id)
+ await writeState(state)
+ api.ui.toast({ variant: "info", message: "Caveman disabled" })
+ },
+ },
+ {
+ title: "Set caveman level",
+ value: "caveman.level",
+ category: "Caveman",
+ slash: { name: "caveman_level" },
+ onSelect: async () => {
+ const id = sid()
+ if (!id) return
+ api.ui.dialog.replace(() =>
+ api.ui.DialogSelect({
+ title: "Caveman level",
+ options: [
+ { title: "full", value: "full", description: "Classic caveman. Drop articles, fragments OK. (default)" },
+ { title: "lite", value: "lite", description: "No filler. Full sentences. Professional." },
+ { title: "ultra", value: "ultra", description: "Max compression. Abbreviations, arrows." },
+ ],
+ onSelect: async (opt) => {
+ api.ui.dialog.clear()
+ const state = await readState()
+ state.levels[id] = opt.value
+ await writeState(state)
+ api.ui.toast({ variant: "success", message: `Caveman level: ${opt.value}` })
+ },
+ }),
+ )
+ },
+ },
+ ])
+}
+
+export default { id: "caveman-tui", tui }