From dd1fab37ae11caf58cde391b3d7a5452b5fd3bcb Mon Sep 17 00:00:00 2001 From: bcode Date: Sat, 16 May 2026 04:46:06 +0000 Subject: [PATCH 1/2] fix(skill): customize-opencode off by default; promote browser-execute guide to a registered skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eval-agent deep-dive on v0.1.6 regressed traces: the only skill registered in browser sessions was upstream's customize-opencode (opencode.json schema authoring) — pure pollution for browser-driving workflows. Meanwhile the genuinely useful browser-execute-guide.md was not a registered skill, only surfaced because the tool description said 'you MUST Read this file first.' Two changes: 1) Gate customize-opencode built-in registration on BCODE_ENABLE_CUSTOMIZE_OPENCODE=1 (default off). A user-disk skill of the same name still loads, since the gate runs before disk discovery. 2) Rename packages/bcode-browser/skills/browser-execute-guide.md → browser-execute/SKILL.md, add frontmatter (name: browser-execute, description front-loads 'Use ONLY when calling browser_execute'), and extend discoverSkills to scan /skills/ where the bcode-browser package already materializes first-party skills. The skill now appears in at planning time and is loaded via the skill tool. Updated browser-execute.txt to instruct 'you MUST use the skill tool first to load the browser-execute skill' — keeps the strong MUST language verbatim per user confirmation that the wording materially improves eval scores. --- packages/bcode-browser/README.md | 2 +- .../SKILL.md} | 5 +++ packages/bcode-browser/src/browser-execute.ts | 4 +- packages/bcode-browser/src/index.ts | 4 +- packages/bcode-browser/test/skills.test.ts | 6 +-- .../test/workspace-import.test.ts | 2 +- packages/opencode/src/skill/index.ts | 38 +++++++++++++++---- .../opencode/src/tool/browser-execute.txt | 2 +- 8 files changed, 46 insertions(+), 17 deletions(-) rename packages/bcode-browser/skills/{browser-execute-guide.md => browser-execute/SKILL.md} (94%) diff --git a/packages/bcode-browser/README.md b/packages/bcode-browser/README.md index 98ff17787..257e24a51 100644 --- a/packages/bcode-browser/README.md +++ b/packages/bcode-browser/README.md @@ -12,7 +12,7 @@ See `decisions.md §1c` (three-level model) and `§1d` (this package) in the Bro | `src/browser-execute.ts` | In-process JS-eval `browser_execute` body. | | `src/session-store.ts` | Per-opencode-session CDP `Session` map. The agent calls `session.connect(...)` from a snippet; subsequent snippets find the same Session. | | `src/skills.ts` | Runtime resolver for embedded skills (extract on first call in compiled mode; in-tree path in dev). | -| `skills/` | `browser-execute-guide.md` (the agent's prompt for `browser_execute`, covering all three connection Ways including Browser Use cloud provisioning via raw HTTP from inside a snippet). Embedded into the binary by `script/embed-skills.ts`. The interaction-skills set inherited from the Python harness was archived 2026-05-09 — we'll reintroduce only what evals show is needed, one skill at a time. | +| `skills/` | `browser-execute/SKILL.md` (the agent-facing reference for `browser_execute`, covering all three connection Ways including Browser Use cloud provisioning via raw HTTP from inside a snippet). Registered as an opencode skill via frontmatter; surfaced in `` and loaded with the `skill` tool. Embedded into the binary by `script/embed-skills.ts`. The interaction-skills set inherited from the Python harness was archived 2026-05-09 — we'll reintroduce only what evals show is needed, one skill at a time. | | `script/embed-skills.ts` | Build-time embed; emits `bcode-skills.gen.ts` consumed by the compiled binary. | | `test/` | `bun test` smoke coverage for the workspace dynamic-import pattern. | diff --git a/packages/bcode-browser/skills/browser-execute-guide.md b/packages/bcode-browser/skills/browser-execute/SKILL.md similarity index 94% rename from packages/bcode-browser/skills/browser-execute-guide.md rename to packages/bcode-browser/skills/browser-execute/SKILL.md index 10254732f..1e6ae048b 100644 --- a/packages/bcode-browser/skills/browser-execute-guide.md +++ b/packages/bcode-browser/skills/browser-execute/SKILL.md @@ -1,3 +1,8 @@ +--- +name: browser-execute +description: Use ONLY when calling the `browser_execute` tool or driving a real browser via the Chrome DevTools Protocol. Required reading before the first `browser_execute` call in a session. Covers the three connection methods (local Chrome with remote debugging, isolated debug-port profile, Browser Use cloud), the in-process `session` / `console` snippet model, attaching to a page target, common CDP commands, the per-project `.bcode/agent-workspace/` for reusable scripts, and screenshot auto-attachment. +--- + The `browser_execute` tool evaluates JavaScript against a connected browser `session` via the Chrome DevTools Protocol. The snippet runs in-process; `session` is bound to a long-lived CDP `Session` that persists. Connect once, then drive many snippets. There is no helper namespace, just `session`, `console`, and standard JS globals. diff --git a/packages/bcode-browser/src/browser-execute.ts b/packages/bcode-browser/src/browser-execute.ts index 51c318f17..230bacbd9 100644 --- a/packages/bcode-browser/src/browser-execute.ts +++ b/packages/bcode-browser/src/browser-execute.ts @@ -56,7 +56,7 @@ const MAX_TIMEOUT_MS = 10 * 60 * 1000 export const parameters = Schema.Struct({ code: Schema.String.annotate({ description: - "The JavaScript snippet to execute. `session` (CDP Session) and `console` are in scope; see browser-execute-guide.md for the snippet model.", + "The JavaScript snippet to execute. `session` (CDP Session) and `console` are in scope; see the `browser-execute` skill for the snippet model.", }), timeout: Schema.optional(Schema.Number).annotate({ description: `Optional timeout in milliseconds (default ${DEFAULT_TIMEOUT_MS}, max ${MAX_TIMEOUT_MS})`, @@ -145,7 +145,7 @@ const serialize = (v: unknown): string => { // Snippet executor. The CDP Session is resolved per-call from `SessionStore` // keyed on `ctx.sessionID`. The agent connects with `await session.connect(...)` -// in one snippet (Way 1 / Way 2 / Way 3 in browser-execute-guide.md); the Session persists +// in one snippet (Way 1 / Way 2 / Way 3 in skills/browser-execute/SKILL.md); the Session persists // for follow-up snippets in the same opencode session. // // `dataDir` is opencode's XDG_DATA_HOME for bcode (~/.local/share/bcode/ on diff --git a/packages/bcode-browser/src/index.ts b/packages/bcode-browser/src/index.ts index ac70f410e..1e513a5d8 100644 --- a/packages/bcode-browser/src/index.ts +++ b/packages/bcode-browser/src/index.ts @@ -11,10 +11,10 @@ // src/browser-execute.ts — in-process JS-eval browser_execute body // src/session-store.ts — per-opencode-session CDP Session map // src/skills.ts — runtime resolver for embedded skills -// skills/ — browser-execute-guide.md (embedded into binary) +// skills/ — browser-execute/SKILL.md (embedded into binary) // // Cloud browser provisioning is intentionally NOT a separate Level-1 -// surface. The agent reads Way 3 of `skills/browser-execute-guide.md` and +// surface. The agent reads Way 3 of `skills/browser-execute/SKILL.md` and // writes the fetch+connect snippet itself, matching how local-browser // connect works (snippet-side, not tool-side). Decisions trail in // `memory/browsercode/decisions.md` §3.4. diff --git a/packages/bcode-browser/test/skills.test.ts b/packages/bcode-browser/test/skills.test.ts index 80a852020..1a6dd2e02 100644 --- a/packages/bcode-browser/test/skills.test.ts +++ b/packages/bcode-browser/test/skills.test.ts @@ -14,7 +14,7 @@ test("resolveSkillsDir materializes skills with {{SKILLS_DIR}} substituted", asy try { const dir = await Skills.resolveSkillsDir(dataDir) expect(dir).toBe(path.join(dataDir, "skills")) - const browser = (await fs.readFile(path.join(dir, "browser-execute-guide.md"), "utf8")).replaceAll("\\", "/") + const browser = (await fs.readFile(path.join(dir, "browser-execute", "SKILL.md"), "utf8")).replaceAll("\\", "/") expect(browser).not.toContain("{{SKILLS_DIR}}") expect(browser).toContain(`${dir.replaceAll("\\", "/")}/`) } finally { @@ -29,8 +29,8 @@ test("different dataDirs get their own substituted paths", async () => { const dirA = await Skills.resolveSkillsDir(a) const dirB = await Skills.resolveSkillsDir(b) const [browserA, browserB] = (await Promise.all([ - fs.readFile(path.join(dirA, "browser-execute-guide.md"), "utf8"), - fs.readFile(path.join(dirB, "browser-execute-guide.md"), "utf8"), + fs.readFile(path.join(dirA, "browser-execute", "SKILL.md"), "utf8"), + fs.readFile(path.join(dirB, "browser-execute", "SKILL.md"), "utf8"), ])).map((s) => s.replaceAll("\\", "/")) const [a2, b2] = [dirA.replaceAll("\\", "/"), dirB.replaceAll("\\", "/")] expect(browserA).toContain(a2) diff --git a/packages/bcode-browser/test/workspace-import.test.ts b/packages/bcode-browser/test/workspace-import.test.ts index 4b0de35d2..3c0e16f23 100644 --- a/packages/bcode-browser/test/workspace-import.test.ts +++ b/packages/bcode-browser/test/workspace-import.test.ts @@ -3,7 +3,7 @@ // at runtime from a `browser_execute` snippet via // `await import("/abs/path?t=" + Date.now())`. We don't run a real // `browser_execute` here — the point is to verify the dynamic-import -// mechanism behaves as the browser-execute-guide.md prompt claims. +// mechanism behaves as the browser-execute skill prompt claims. // // All four scenarios run against a real tmp dir, real .ts files, and // the real Bun module loader. No mocks. diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index 734770794..8bc19a6ee 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -14,6 +14,7 @@ import { ConfigMarkdown } from "@/config/markdown" import { RuntimeFlags } from "@/effect/runtime-flags" import { Glob } from "@opencode-ai/core/util/glob" import * as Log from "@opencode-ai/core/util/log" +import { Skills as BcodeSkills } from "@browser-use/bcode-browser/skills" import { Discovery } from "./discovery" import CUSTOMIZE_OPENCODE_SKILL_BODY from "./prompt/customize-opencode.md" with { type: "text" } import { isRecord } from "@/util/record" @@ -216,6 +217,16 @@ const discoverSkills = Effect.fnUntraced(function* ( } } + // BrowserCode-shipped skills (browser-execute and any future first-party + // reference docs) live at /skills//SKILL.md after the + // bcode-browser materialization step. Scan unconditionally — the dir may + // not exist yet on the very first launch before BrowserExecute.make has + // run, and that's fine (Glob returns empty). + const bcodeSkillsDir = BcodeSkills.skillsDir(global.data) + if (yield* fsys.isDir(bcodeSkillsDir)) { + yield* scan(state, bcodeSkillsDir, SKILL_PATTERN, { scope: "bcode" }) + } + return { matches: Array.from(state.matches), dirs: Array.from(state.dirs), @@ -258,13 +269,26 @@ export const layer = Layer.effect( const state = yield* InstanceState.make( Effect.fn("Skill.state")(function* () { const s: State = { skills: {}, dirs: new Set() } - // Register the built-in skill BEFORE disk discovery so a user-disk - // skill with the same name can override it. - s.skills[CUSTOMIZE_OPENCODE_SKILL_NAME] = { - name: CUSTOMIZE_OPENCODE_SKILL_NAME, - description: CUSTOMIZE_OPENCODE_SKILL_DESCRIPTION, - location: "", - content: CUSTOMIZE_OPENCODE_SKILL_BODY, + // BrowserCode gate: the upstream `customize-opencode` built-in is + // off-by-default in BrowserCode. The skill describes opencode.json, + // opencode plugins, opencode agents — not useful for browser-driving + // sessions, and registering it unconditionally pollutes the system + // prompt with negative-signal content (eval data showed a measurable + // regression on browser-task scores when this skill was forced on). + // Set BCODE_ENABLE_CUSTOMIZE_OPENCODE=1 to opt back in for sessions + // where the user is actually editing bcode.json or agent configs. + // Skipped registration happens BEFORE disk discovery, so a user-disk + // skill named `customize-opencode` still loads normally. + const customizeEnabled = + process.env.BCODE_ENABLE_CUSTOMIZE_OPENCODE === "1" || + process.env.BCODE_ENABLE_CUSTOMIZE_OPENCODE?.toLowerCase() === "true" + if (customizeEnabled) { + s.skills[CUSTOMIZE_OPENCODE_SKILL_NAME] = { + name: CUSTOMIZE_OPENCODE_SKILL_NAME, + description: CUSTOMIZE_OPENCODE_SKILL_DESCRIPTION, + location: "", + content: CUSTOMIZE_OPENCODE_SKILL_BODY, + } } yield* loadSkills(s, yield* InstanceState.get(discovered), bus) return s diff --git a/packages/opencode/src/tool/browser-execute.txt b/packages/opencode/src/tool/browser-execute.txt index 758aadd35..3a11ae174 100644 --- a/packages/opencode/src/tool/browser-execute.txt +++ b/packages/opencode/src/tool/browser-execute.txt @@ -3,5 +3,5 @@ Executes JavaScript in a browser via CDP. Usage: - Use this tool whenever the task requires driving a real browser. - Use this tool to read webpages that block the webfetch tool. -- IMPORTANT: you MUST use the Read tool first to read `{{SKILLS_DIR}}/browser-execute-guide.md`. This tool will fail if you did not read these directions first. +- IMPORTANT: you MUST use the skill tool first to load the `browser-execute` skill. This tool will fail if you did not read those directions first. - Returns console output from the snippet; screenshots taken attach automatically as images. \ No newline at end of file From a6ac76a905918324be5ce07d07e5f148e15fdb0b Mon Sep 17 00:00:00 2001 From: bcode Date: Sat, 16 May 2026 04:50:20 +0000 Subject: [PATCH 2/2] simplify: delete customize-opencode entirely, no env-var gate Per user: env-var gate added unnecessary surface area. The skill teaches opencode.json schema authoring; for BrowserCode that's the wrong product surface, so don't ship it at all. Removes the const + import + registration block (about 17 lines), and the now-orphaned 377-line prompt body. Net diff: -388 lines. --- packages/opencode/src/skill/index.ts | 37 +- .../src/skill/prompt/customize-opencode.md | 377 ------------------ 2 files changed, 6 insertions(+), 408 deletions(-) delete mode 100644 packages/opencode/src/skill/prompt/customize-opencode.md diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index 8bc19a6ee..6c6986388 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -16,7 +16,6 @@ import { Glob } from "@opencode-ai/core/util/glob" import * as Log from "@opencode-ai/core/util/log" import { Skills as BcodeSkills } from "@browser-use/bcode-browser/skills" import { Discovery } from "./discovery" -import CUSTOMIZE_OPENCODE_SKILL_BODY from "./prompt/customize-opencode.md" with { type: "text" } import { isRecord } from "@/util/record" const log = Log.create({ service: "skill" }) @@ -26,15 +25,6 @@ const EXTERNAL_SKILL_PATTERN = "skills/**/SKILL.md" const OPENCODE_SKILL_PATTERN = "{skill,skills}/**/SKILL.md" const SKILL_PATTERN = "**/SKILL.md" -// Built-in skill that ships with opencode. The model's intuition for what an -// opencode.json should look like is often wrong, and opencode hard-fails on -// invalid config, so users hit cryptic startup errors. Loading this skill -// when the model is asked to touch opencode's own config files gives it the -// actual schemas instead of guesses. -const CUSTOMIZE_OPENCODE_SKILL_NAME = "customize-opencode" -const CUSTOMIZE_OPENCODE_SKILL_DESCRIPTION = - "Use ONLY when the user is editing or creating opencode's own configuration: opencode.json, opencode.jsonc, files under .opencode/, or files under ~/.config/opencode/. Also use when creating or fixing opencode agents, subagents, skills, plugins, MCP servers, or permission rules. Do not use for the user's own application code, or for any project that is not configuring opencode itself." - export const Info = Schema.Struct({ name: Schema.String, description: Schema.optional(Schema.String), @@ -269,27 +259,12 @@ export const layer = Layer.effect( const state = yield* InstanceState.make( Effect.fn("Skill.state")(function* () { const s: State = { skills: {}, dirs: new Set() } - // BrowserCode gate: the upstream `customize-opencode` built-in is - // off-by-default in BrowserCode. The skill describes opencode.json, - // opencode plugins, opencode agents — not useful for browser-driving - // sessions, and registering it unconditionally pollutes the system - // prompt with negative-signal content (eval data showed a measurable - // regression on browser-task scores when this skill was forced on). - // Set BCODE_ENABLE_CUSTOMIZE_OPENCODE=1 to opt back in for sessions - // where the user is actually editing bcode.json or agent configs. - // Skipped registration happens BEFORE disk discovery, so a user-disk - // skill named `customize-opencode` still loads normally. - const customizeEnabled = - process.env.BCODE_ENABLE_CUSTOMIZE_OPENCODE === "1" || - process.env.BCODE_ENABLE_CUSTOMIZE_OPENCODE?.toLowerCase() === "true" - if (customizeEnabled) { - s.skills[CUSTOMIZE_OPENCODE_SKILL_NAME] = { - name: CUSTOMIZE_OPENCODE_SKILL_NAME, - description: CUSTOMIZE_OPENCODE_SKILL_DESCRIPTION, - location: "", - content: CUSTOMIZE_OPENCODE_SKILL_BODY, - } - } + // BrowserCode-specific: the upstream `customize-opencode` built-in + // registration was removed here. The skill teaches the model + // opencode.json / opencode plugin authoring and is irrelevant to + // browser-driving workflows; eval traces showed it correlated with + // a score regression. A user-disk skill of the same name still + // loads normally through the regular discovery path. yield* loadSkills(s, yield* InstanceState.get(discovered), bus) return s }), diff --git a/packages/opencode/src/skill/prompt/customize-opencode.md b/packages/opencode/src/skill/prompt/customize-opencode.md deleted file mode 100644 index 4ba118b09..000000000 --- a/packages/opencode/src/skill/prompt/customize-opencode.md +++ /dev/null @@ -1,377 +0,0 @@ - - -# Customizing opencode - -opencode validates its own config strictly and refuses to start when a field -is wrong. The shapes below cover the common surface area, but they are a -**summary, not the source of truth**. - -## Full schema reference - -The authoritative list of every config option — with field types, enums, -defaults, and descriptions — lives in the published JSON Schema: - -**** - -If a field is not documented in this skill, or you need to confirm an exact -shape before writing config, **fetch that URL and read the schema directly** -rather than guessing. opencode hard-fails on invalid config, so the cost of a -wrong shape is a broken startup. - -Independently, every `opencode.json` should declare -`"$schema": "https://opencode.ai/config.json"` so the user's editor catches -mistakes as they type. - -## Applying changes - -Config is loaded once when opencode starts and is not hot-reloaded. After -saving changes to `opencode.json`, an agent file, a skill, a plugin, or any -other config-time file, **tell the user to quit and restart opencode** for -the changes to take effect. The running session will keep using the -already-loaded config until then. - -## Where files live - -| Scope | Path | -| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -| Project config | `./opencode.json`, `./opencode.jsonc`, or `.opencode/opencode.json` (opencode walks up from the cwd to the worktree root) | -| Global config | `~/.config/opencode/opencode.json` (NOT `~/.opencode/`) | -| Project agents | `.opencode/agent/.md` or `.opencode/agents/.md` | -| Global agents | `~/.config/opencode/agent(s)/.md` | -| Project skills | `.opencode/skill(s)//SKILL.md` | -| Global skills | `~/.config/opencode/skill(s)//SKILL.md` | -| External skills (auto-loaded) | `~/.claude/skills//SKILL.md`, `~/.agents/skills//SKILL.md` | - -Configs from each scope are deep-merged. Project overrides global. Unknown -top-level keys in `opencode.json` are rejected with `ConfigInvalidError`. - -## opencode.json - -Every field is optional. - -```json -{ - "$schema": "https://opencode.ai/config.json", - "username": "string", - "model": "provider/model-id", - "small_model": "provider/model-id", - "default_agent": "agent-name", - "shell": "/bin/zsh", - "logLevel": "DEBUG" | "INFO" | "WARN" | "ERROR", - "share": "manual" | "auto" | "disabled", - "autoupdate": true | false | "notify", - "snapshot": true, - "instructions": ["AGENTS.md", "docs/style.md"], - - "skills": { - "paths": [".opencode/skills", "/abs/path/to/skills"], - "urls": ["https://example.com/.well-known/skills/"] - }, - - "agent": { - "my-agent": { - "model": "anthropic/claude-sonnet-4-6", - "mode": "subagent", - "description": "...", - "permission": { "edit": "deny" } - } - }, - - "command": { - "deploy": { "description": "...", "prompt": "..." } - }, - - "provider": { - "anthropic": { "options": { "apiKey": "..." } } - }, - "disabled_providers": ["openai"], - "enabled_providers": ["anthropic"], - - "mcp": { - "playwright": { - "type": "local", - "command": ["npx", "-y", "@playwright/mcp"], - "enabled": true, - "env": {} - }, - "remote-thing": { - "type": "remote", - "url": "https://...", - "headers": { "Authorization": "Bearer ..." } - } - }, - - "plugin": [ - "opencode-gemini-auth", - "opencode-foo@1.2.3", - "./local-plugin.ts", - ["opencode-bar", { "option": "value" }] - ], - - "permission": { - "edit": "deny", - "bash": { "git *": "allow", "*": "ask" } - }, - - "formatter": false, - "lsp": false, - - "experimental": { - "primary_tools": ["edit"], - "mcp_timeout": 30000 - }, - - "tool_output": { "max_lines": 200, "max_bytes": 8192 }, - - "compaction": { "auto": true, "tail_turns": 15 } -} -``` - -Shape notes worth being explicit about: - -- `model` always carries a provider prefix: `"anthropic/claude-sonnet-4-6"`. -- `skills` is an object with `paths` and/or `urls`, not an array. -- `agent` is an object keyed by agent name, not an array. -- `plugin` is an array of strings or `[name, options]` tuples, not an object. -- `mcp[name].command` is an array of strings, never a single string. `type` is required. -- `permission` is either a string action or an object keyed by tool name. - -## Skills - -opencode's skill loader scans for `**/SKILL.md` inside skill directories. The -file is named `SKILL.md` exactly, and lives in its own folder named after the -skill: - -``` -.opencode/skills/my-skill/SKILL.md -``` - -Frontmatter: - -```markdown ---- -name: my-skill -description: One sentence covering what this skill does AND when to trigger it. Front-load the literal keywords or filenames the user is likely to say. ---- - -# My Skill - -(skill body in markdown: instructions, examples, references) -``` - -- `name` is required, lowercase hyphen-separated, up to 64 chars, and matches the folder name. -- `description` is effectively required: skills without one are filtered out and never surfaced to the model. Cover both _what_ the skill does and _when_ to use it. Write in third person ("Use when...", not "I help with..."). Front-load concrete trigger keywords and filenames; gate with "Use ONLY when..." if the skill should stay quiet on adjacent topics. -- Optional: `license`, `compatibility`, `metadata` (string-string map). - -Register skills from non-default locations via `skills.paths` (scanned -recursively for `**/SKILL.md`) and `skills.urls` (each URL serves a list of -skills). - -## Agents - -Two ways to define an agent. Use the file form for anything non-trivial. - -### Inline (in `opencode.json`) - -```json -{ - "agent": { - "my-reviewer": { - "description": "Reviews PRs for style violations.", - "mode": "subagent", - "model": "anthropic/claude-sonnet-4-6", - "permission": { "edit": "deny", "bash": "ask" }, - "prompt": "You are a strict PR reviewer..." - } - } -} -``` - -### File - -``` -.opencode/agent/my-reviewer.md OR .opencode/agents/my-reviewer.md -``` - -```markdown ---- -description: Reviews PRs for style violations. -mode: subagent -model: anthropic/claude-sonnet-4-6 -permission: - edit: deny - bash: ask ---- - -You are a strict PR reviewer. Focus on... -``` - -The file body becomes the agent's `prompt`. Do not also put `prompt:` in the -frontmatter. - -`mode` is one of `"primary"`, `"subagent"`, `"all"`. - -Allowed top-level frontmatter fields: `name, model, variant, description, mode, -hidden, color, steps, options, permission, disable, temperature, top_p`. Any -unknown field is silently routed into `options`. - -To disable a built-in agent: `agent: { build: { disable: true } }`, or in a -file, `disable: true` in frontmatter. - -`default_agent` must point to a non-hidden, primary-mode agent. - -### Built-in agents - -opencode ships with `build`, `plan`, `general`, `explore`, plus optionally -`scout` (gated on `OPENCODE_EXPERIMENTAL_SCOUT`). Hidden internal agents: -`compaction`, `title`, `summary`. To override a built-in's fields, define the -same key in `agent: { : { ... } }`. - -## Plugins - -`plugin:` is an array. Each entry is one of: - -```json -"plugin": [ - "opencode-gemini-auth", // npm spec, latest - "opencode-foo@1.2.3", // npm spec, pinned - "./local-plugin.ts", // file path, relative to the declaring config - "file:///abs/path/plugin.js", // file URL - ["opencode-bar", { "key": "val" }] // tuple form with options -] -``` - -Auto-discovered plugins (no config entry needed): any `*.ts` or `*.js` file in -`.opencode/plugin/` or `.opencode/plugins/`. - -A plugin module exports `default` (or any named export) of type -`Plugin = (input: PluginInput, options?) => Promise`. The export is a -function, not a plain object literal, and the function returns an object -(return `{}` if there is nothing to register). - -```ts -import type { Plugin } from "@opencode-ai/plugin" - -export default (async ({ client, project, directory, $ }) => { - return { - config: (cfg) => { - // cfg is the live merged config; mutate fields here. - }, - "tool.execute.before": async (input, output) => { - // mutate output.args before the tool runs - }, - } -}) satisfies Plugin -``` - -Hook surface (mutate `output` in place; return `void`): - -- `event(input)`: every bus event -- `config(cfg)`: once on init with the merged config -- `chat.message`, `chat.params`, `chat.headers` -- `tool.execute.before`, `tool.execute.after` -- `tool.definition` -- `command.execute.before` -- `shell.env` -- `permission.ask` -- `experimental.chat.messages.transform`, `experimental.chat.system.transform`, - `experimental.session.compacting`, `experimental.compaction.autocontinue`, - `experimental.text.complete` - -Special object-shaped (not callbacks): `tool: { my_tool: { ... } }`, -`auth: { ... }`, `provider: { ... }`. - -## MCP servers - -`mcp:` is an object keyed by server name. Each server is discriminated by -`type`: - -```json -{ - "mcp": { - "playwright": { - "type": "local", - "command": ["npx", "-y", "@playwright/mcp"], - "enabled": true, - "env": { "BROWSER": "chromium" } - }, - "github": { - "type": "remote", - "url": "https://...", - "enabled": true, - "headers": { "Authorization": "Bearer ${GITHUB_TOKEN}" } - }, - "old-server": { "enabled": false } - } -} -``` - -`command` is an array of strings. `type` is required. Use `enabled: false` to -disable a server inherited from a parent config. - -## Permissions - -```json -"permission": { - "edit": "deny", - "bash": { "git *": "allow", "rm *": "deny", "*": "ask" }, - "external_directory": { "~/secrets/**": "deny", "*": "allow" } -} -``` - -Actions: `"allow"`, `"ask"`, `"deny"`. - -Per-tool value forms: `"allow"` shorthand (treated as `{"*": "allow"}`), or an -object `{ pattern: action }`. Within an object, **insertion order matters**. -opencode evaluates the LAST matching rule, so put broad rules first and narrow -rules last. - -`permission: "allow"` (a string at the top level) is shorthand for "allow -everything" and is rarely what the user wants. - -Known permission keys: `read, edit, glob, grep, list, bash, task, -external_directory, todowrite, question, webfetch, websearch, repo_clone, -repo_overview, lsp, doom_loop, skill`. Some of these (`todowrite, -question, webfetch, websearch, doom_loop`) only accept a flat -action, not a per-pattern object. - -`external_directory` patterns are filesystem paths (use `~/`, absolute paths, -or globs like `~/projects/**`). - -Per-agent `permission:` overrides top-level `permission:`. Plan Mode lives on -the `plan` agent's permission ruleset (`edit: deny *`). - -## Escape hatches - -When a user's config is broken and opencode won't start, these env vars help: - -- `OPENCODE_DISABLE_PROJECT_CONFIG=1`: skip the project's local `opencode.json` - and start from globals only. Run from the project directory, opencode loads, - the user edits the broken file, then they restart without the flag. -- `OPENCODE_CONFIG=/path/to/file.json`: load an additional explicit config. -- `OPENCODE_CONFIG_CONTENT='{"$schema":"https://opencode.ai/config.json"}'`: - inject inline JSON as a final local-scope merge. -- `OPENCODE_DISABLE_DEFAULT_PLUGINS=1`: skip default plugins. -- `OPENCODE_PURE=1`: skip external plugins entirely. -- `OPENCODE_DISABLE_EXTERNAL_SKILLS=1`, - `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS=1`: skip the external skill scans under - `~/.claude/` and `~/.agents/`. - -## When proposing edits - -- Validate against the schema before writing. If you are unsure of a field's - exact shape, or the field is not covered in this skill, fetch - `https://opencode.ai/config.json` and read the schema rather than guessing. -- Preserve `$schema` and any existing fields the user did not ask to change. -- For agent, skill, and plugin definitions, prefer creating new files in the - correct location over inlining everything in `opencode.json`. -- If the user's existing config is malformed, point them at the env-var escape - hatches above so they can edit from inside opencode without breaking their - session. -- After saving any config change, remind the user to quit and restart opencode - — running sessions keep using the already-loaded config.