Ready-made presets for OpenCode: permission rules, MCP servers, LSP
overrides, agent limits, TUI preferences. A small CLI merges them into
your opencode.json and tui.json so you never hand-edit the JSON.
npm install -g opencode-presetsNeeds Node 22+.
Fresh install? Start with the permission bundle — one command, no prompts for the everyday read-only commands, hard blocks on the destructive ones, and session sharing turned off:
opencode-presets install permissions-recommendedIt is opinionated, and it is not a free pass. These are one person's
defaults for everyday work, not an audited sandbox and not a security boundary.
They cut prompting for read-only commands, hard-block a list of known
footguns, and set share to "disabled" — that is the whole claim. Anything
not listed still falls through to a prompt you have to read; the deny rules
miss env-prefixed and sh -c-wrapped invocations; and the presets you add on
top can undo them. Read the rules before trusting them, and keep deciding for
yourself.
Install any row below with opencode-presets install <preset>. Presets that need
something outside your opencode config say so in their description.
| Preset | Category | Mode | Description |
|---|---|---|---|
permissions-recommended |
Permissions | bundle | Start here. Installs the seven presets marked In the bundle |
permissions-shell-safe |
Permissions | merge | In the bundle. Low-risk shell commands (ls, cat, grep, rg, jq, yq, etc.) |
permissions-git-safe |
Permissions | merge | In the bundle. Read-only git commands (status, diff, log, branch --list, fetch, etc.) |
permissions-toolchain-info |
Permissions | merge | In the bundle. Version probes for common dev toolchains |
permissions-container-info |
Permissions | merge | In the bundle. Read-only docker and podman inspection commands (the oc rules moved to permissions-cluster-info in 0.2.0) |
permissions-deny-destructive |
Permissions | merge | In the bundle. Hard-denies sudo, root/home-anchored rm -rf, dd, mkfs, force-push, reset --hard |
permissions-deny-cluster-write |
Permissions | merge | In the bundle. Hard-denies mutating and exec oc, kubectl, helm verbs — no prompt, not bypassable by --auto |
permissions-build-tools |
Permissions | merge | Not in the bundle. Build tools (node, npm, mvn, gradle, make, python, pip, cargo, go) |
permissions-cluster-info |
Permissions | merge | Not in the bundle. Read-only oc (OpenShift) inspection — grants read access to whichever cluster you are logged into |
permissions-webfetch-ask |
Permissions | merge | Not in the bundle. Requires approval before opencode uses the webfetch tool |
jdtls-lombok |
LSP | replace | Needs jdtls and a JDK 21+ on PATH already. Makes jdtls lombok-aware via a -javaagent flag (pins lombok 1.18.46, sha256-verified) |
jdtls-clean-workspace |
LSP | replace | Stops jdtls from writing .project/.classpath/etc. into your project root |
mcp-http |
MCP | replace | Add an HTTP MCP server (localhost or remote) with one custom header (prompts for id, URL, header name, header value) |
mcp-http-noauth |
MCP | replace | Add an HTTP MCP server (localhost or remote) without auth headers (prompts for id, URL) |
mcp-intellij |
MCP | replace | Requires the official "MCP Server" plugin installed and enabled in the IDE first — this preset does not install it. Adds the JetBrains IDE MCP server (loopback HTTP, default port 64342) |
mcp-litellm |
MCP | replace | Add a LiteLLM proxy's MCP gateway as a remote MCP server (prompts for gateway URL and LiteLLM key; auth via x-litellm-api-key, no login flow) |
mcp-litellm-passthrough |
MCP | replace | Install mcp-litellm first — re-running it replaces mcp.litellm and drops these headers. Adds one x-mcp-<alias>-<header> passthrough header to the mcp.litellm server so an upstream MCP server authenticates as you (run once per header) |
mcp-playwright |
MCP | replace | Add the Playwright MCP server (local stdio via npx; pins @playwright/mcp 0.0.79) |
mcp-vscode |
MCP | replace | Requires the JuehangQin.vscode-mcp-server extension installed, enabled and toggled active in VS Code first — this preset does not install it. Adds the VS Code MCP server via that extension (loopback HTTP, default port 3000) |
plugin-litellm-pricing |
Plugin | append | Install provider-litellm too — without a litellm provider pointing at your proxy the plugin does nothing. Adds opencode-plugin-litellm-pricing: discovers a LiteLLM proxy's models at runtime and adds them to the picker with the proxy's own per-model pricing instead of $0 (pins opencode-plugin-litellm-pricing 0.9.0) |
provider-litellm |
Provider | replace | Point the litellm provider at your proxy URL and key for plugin-litellm-pricing, which prices the models against that same proxy (prompts for base URL and API key; no models list) |
plugin-superpowers |
Plugin | append | Add the Superpowers OpenCode plugin from obra/superpowers (brainstorming, plans, TDD, review workflows; pins tag v6.3.0) |
privacy-share-disabled |
Privacy | replace | In the bundle. Sets share to "disabled" so opencode never publishes a session, automatically or on command |
agent-runaway-guard |
Agent | merge | Adds step limits to built-in agents to prevent runaway tool loops |
default-agent-plan |
Agent | replace | Sets the default agent to "plan" so opencode always starts in plan mode instead of build mode |
opencode-planify-german |
Bundle | — | The whole planify setup: plugin-opencode-planify-german, instructions-opencode-planify-german, skill-opencode-planify-german. Install all three or nothing happens — the rules name a tool that would not exist, the plugin would never be called |
plugin-opencode-planify-german |
Plugin | append | Needs instructions-opencode-planify-german too, or nothing tells the agent to use the tool — install the opencode-planify-german bundle for both. Adds the opencode-planify-german plugin from trick77/opencode-planify-german, which registers the plan_render tool: takes a plan as JSON, validates it against a schema, renders a self-contained HTML file to docs/plans/<TICKET>-<slug>.html and opens it in the system's default browser (installs it from npm, pinned to 0.3.2; restart opencode after installing — plugins are resolved at start) |
instructions-opencode-planify-german |
Instructions | append | Needs plugin-opencode-planify-german — these rules name the plan_render tool it provides. Answer in German with Swiss orthography (never the eszett character, always ss; umlauts as real characters, never ae/oe/ue), German code comments with German domain nouns in identifiers, and every plan built as JSON and handed to plan_render instead of hand-written HTML or Markdown, with <TICKET> taken from the current branch name (fetches the rules file from trick77/opencode-planify-german, sha256-verified). Replaces the former instructions-swiss-rules |
skill-opencode-planify-german |
Skill | append | Registers the planify skill — the plan JSON schema field by field, the writing rules per field, when a plan gets a diagram, and a complete example. Fetches the skill files from trick77/opencode-planify-german into the cache, sha256-verified, and appends that dir to skills.paths; no clone needed. Uninstall by deleting the one skills.paths entry by hand |
skill-diagram-design |
Skill | append | Needs a clone of the repo on disk first — git clone https://github.com/cathrynlavery/diagram-design ~/src/diagram-design. Registers the diagram-design skill (editorial diagram types as self-contained HTML + SVG) by appending your clone's skills/ dir to skills.paths; answer with that clone's skills/ dir (--set skillsDir=/Users/you/src/diagram-design/skills); the install refuses if the dir is not there. Tracks main — the repo ships no tags. One-way: remove cannot undo an append preset that prompts, so uninstall by deleting the one skills.paths entry by hand |
tui-disable-mouse |
TUI | replace | Disables TUI mouse capture so native terminal selection and scrolling keep working |
A preset whose header is @include lines is a bundle: a list of other
presets, with no rules of its own. Two ship: opencode-planify-german, and
permissions-recommended, which is all of this:
permissions-recommended
permissions-shell-safe 22 keys ls, cat, grep, rg, jq, ps allow
permissions-git-safe 40 keys status, diff, log, blame, fetch allow
permissions-toolchain-info 60 keys node -v, python -V, mvn -v allow
permissions-container-info 52 keys docker/podman ps, logs, inspect allow
permissions-deny-destructive 34 keys sudo, dd, mkfs, rm -rf /, -f deny
permissions-deny-cluster-write 64 keys oc/kubectl/helm delete, exec deny
privacy-share-disabled 1 key share disabled
The order is part of the definition: opencode is last-match-wins and merge
appends new keys at the end, so the denies have to land after every allow.
opencode-presets install permissions-recommended
opencode-presets install permissions-recommended permissions-build-toolsReset first? Only if you already have hand-written permission.bash rules.
merge never overwrites, so any rule of yours with the same pattern string as a
preset's keeps that rule out — and when the casualty is a deny, a guardrail you
think you installed is absent. Wiping the path first guarantees every rule lands:
jq '.permission.bash, .agent' ~/.config/opencode/opencode.json # see what you'd lose
opencode-presets install --reset permission.bash permissions-recommendedThat deletes every hand-written rule at permission.bash — a backup is written
first, and nothing else in the config is touched. On a config with no bash rules
of your own it changes nothing, so skip it. Either way the installer names any
deny that was kept out, so you can start with a plain install and only reset if
it complains.
remove expands a bundle the same way. There is no per-preset ownership
tracking, so removing it also clears keys an earlier standalone install of a
member wrote — the confirmation lists every preset first.
Not in the bundle, on purpose:
permissions-build-tools— runs project-defined code, andpython -c "…"/node -e "…"execute commands opencode never sees as shell commands, so no deny rule can match them.permissions-cluster-info— read access to whichever cluster you are logged into, production included. Worth an explicit decision.permissions-webfetch-ask— adds friction; wrong for a defaults bundle.
A denied command is rejected outright — no prompt, and --auto only
auto-approves what is not explicitly denied. That is the one tier a habit of
clicking "allow" cannot defeat. Compound commands are split before matching, so
cd /x && oc delete pod y is caught too.
It is a guardrail, not a security boundary: env-prefixed
(KUBECONFIG=x oc delete …), sh -c "…"-wrapped and aliased invocations slip
through.
Every shipped deny pattern is disjoint from every shipped allow pattern
(enforced by a test), so install order does not matter among these presets. A
broad hand-written rule of your own, like "oc *": "allow", still wins if it
was written after the deny — install the deny presets last.
When one of your rules keeps a deny out, the installer says so instead of letting the guardrail go missing quietly:
• permissions-deny-destructive — added 27, preserved 3
⚠ "sudo *" is already "ask" in your config — the deny was NOT applied
⚠ "rm -rf /" is already "allow" in your config — the deny was NOT applied
To apply those denies, pick one:
1. delete the listed keys from permission.bash in ~/.config/opencode/opencode.json,
then re-run: opencode-presets install permissions-deny-destructive
2. wipe the whole path and reinstall from scratch:
opencode-presets install --reset permission.bash permissions-deny-destructive
this also deletes any other hand-written rules at that path
3. keep your rule deliberately — nothing to do, but the guardrail is off
It also warns before you confirm if any agent sets its own permission rules:
agent.<name>.permission is evaluated after the global rules and wins, so global
denies do nothing for that agent.
Upgrading permissions-container-info to 0.2.0 does not revoke oc access an
earlier install already granted — merge never removes keys, and 0.2.0 no
longer lists the oc rules. Clear them with
opencode-presets remove permissions-cluster-info.
Install multiple at once:
opencode-presets install jdtls-lombok jdtls-clean-workspacePresets whose path uses a prompt (like mcp-http) can't be
removed with remove — use reset instead:
opencode-presets reset mcp.openrag-tomPricing takes no configuration of its own. plugin-litellm-pricing reads each
model's cost, limits and capabilities from the proxy provider-litellm already
points at, so the base URL and key are the whole setup. The numbers are
LiteLLM's own resolved ones, your config-level model_info overrides included,
so what opencode displays is what the gateway bills. A model the proxy reports
no cost for is injected without a cost block rather than with a wrong one, and
the startup log names it:
grep litellm-pricing ~/.local/share/opencode/log/opencode.logprovider-litellm is a replace preset, so re-running it rewrites the whole
provider.litellm block and re-prompts for the base URL and key as well — have
the key to hand. Coming from an earlier install, re-run it once: that is what
clears the now-unused options.catalogURL out of your config.
opencode-presets list # what's available
opencode-presets install jdtls-lombok # apply one preset by name
opencode-presets install jdtls-lombok permissions-git-safe
opencode-presets remove jdtls-lombok # undo a preset
opencode-presets install --reset permission ./presets/foo.conf # wipe then install
opencode-presets reset permission # wipe a section outright
opencode-presets validate # check opencode.json and tui.jsonBare names are resolved through the preset search path (see "Where
presets are found" below). You can always pass an explicit path
instead, e.g. install ./presets/jdtls-lombok.conf.
Every change shows a diff and asks before touching anything. A
backup is written to ~/.cache/opencode-presets/backups/ before
each write — no auto-pruning, so they pile up.
validate checks the configured opencode.json and tui.json
against OpenCode's current schemas. Use validate config,
validate tui, or validate all to choose targets. Missing files
are skipped in all mode; invalid files print labeled where,
what, and detail lines and exit nonzero.
Presets with @prompt directives normally ask interactively. To
drive them from a script (or just paste a one-liner from a wiki),
pre-fill any prompt with --set NAME=VALUE:
opencode-presets install mcp-http \
--set name=openrag \
--set url=https://openrag.example.internal/mcp \
--set headerName=X-Bitbucket-Token \
--set 'headerValue=raw-token-here'Quote values that contain shell metacharacters ($, !, *,
backticks, spaces, etc.) with single quotes — otherwise the shell
expands them before opencode-presets ever sees the value. A
Bitbucket PAT that starts with $ will silently turn into an empty
string without quoting.
For secrets, prefer --set-env NAME=ENV_VAR. The CLI reads the
value from the named environment variable at install time, so the
token never appears in shell history or process listings:
export BITBUCKET_TOKEN=…
opencode-presets install mcp-http \
--set name=openrag \
--set url=https://openrag.example.internal/mcp \
--set headerName=X-Bitbucket-Token \
--set-env headerValue=BITBUCKET_TOKEN--set / --set-env apply to a single preset per invocation — run
the command once per preset rather than bundling several with shared
flags. This keeps the wiring obvious ("this --set goes to that
preset") and avoids surprise: in a non-TTY shell script, a bundled
install would happily fill the first preset's prompts and then hang
on a readline for the next.
replace— the preset owns the value at@path. Re-installing overwrites whatever's there.merge— the preset's keys are added; existing keys (yours or someone else's) are never overwritten. Use this for permission rules so user edits stick around.append— the preset's array entries are appended if missing; existing array entries are preserved. Use this for shared arrays likeplugin. An entry of the formname@versionsupersedes every existing entry naming the same package, in place: bumping a pinned plugin replaces the old pin instead of leaving both in the array for opencode to load twice. A config that already stacked several bumps collapses to the newest one on the next install.
Re-installing is always safe: a no-op produces no backup and no write.
Plugin changes are loaded by opencode at startup. After installing a
plugin preset such as plugin-superpowers, quit and restart opencode.
opencode-presets list searches dirs in this order:
- Anything in
$OPENCODE_PRESETS_PATH(colon-separated). ./presets/relative to your current directory (honoured when it exists).- The shipped
presets/baked into the tool.
Earlier dirs win on name collision; the lower one is still listed
but flagged shadowed. Pass a positional arg (opencode-presets list ~/some/dir) to scan exactly one dir instead.
For a team or cross-machine setup, keep your presets in their own
git repo (not in ~/.config, not inside the cloned tool). Point
the env var at it:
# ~/.zshrc or similar
export OPENCODE_PRESETS_PATH="$HOME/work/team-opencode-presets"Multiple repos? Colon-separate them, highest priority first:
export OPENCODE_PRESETS_PATH="$HOME/personal-presets:$HOME/work/team-presets"For ad-hoc presets you don't want to put in a repo and don't need
on other machines, drop them in ./presets/ from wherever you run
the tool, or use OPENCODE_PRESETS_PATH.
OPENCODE_CONFIG=/path/to/other-opencode.json opencode-presets install ...
OPENCODE_PRESETS_CACHE=/some/cache opencode-presets install ...TUI presets target ~/.config/opencode/tui.json by default. Override
that path with OPENCODE_TUI_CONFIG:
OPENCODE_TUI_CONFIG=/path/to/tui.json opencode-presets install tui-disable-mousePlain JSONC with a header. Drop into one of the dirs above, or pass an absolute path.
@target is optional and defaults to config, which writes
opencode.json. Use @target: tui for TUI presets that write
tui.json. A single install or remove operation cannot mix config
and tui presets; run separate commands for those.
@fetch: <url> -> <dest> [sha256=hex] downloads to the cache.
@prompt: name | text|secret|dir | help | default | setup collects input
at install time; everything after the type is optional. Both repeatable.
Reference fetched files as {{cache}}/<name> and prompt values as
{{prompt:<name>}} in the body or @path. Leave the default field empty
to skip it — name | dir | help | | <setup> has a setup hint and no
default. Only the first four | are structural, so a setup hint may
contain pipes of its own.
A dir prompt is checked before anything is written: the answer must be
an absolute path (a leading ~ is expanded, since prompt input never
sees a shell) to a directory that exists, or the install stops with an
error and leaves your config alone. The resolved path is what gets
written, so the entry means the same thing from any directory. --set
values are checked exactly like typed ones.
The setup field is what to do to make that directory exist. It is shown
in the install summary, before you are asked anything, and again if the
answer fails its check:
error: prompt "skillsDir": /tmp/dd does not exist
clone it first: git clone https://github.com/cathrynlavery/diagram-design, then answer with the skills/ dir inside that clone
nothing was written.
@requires-bin: <name> | <setup> names an executable the preset needs on
PATH, and the command that installs it. Repeatable. The check runs
before the install summary, so a missing binary refuses without ever
asking you to confirm; remove never checks, so you can always take one
back out. It is a name resolved against PATH, not a path — the parser
rejects /usr/local/bin/foo. The setup half is optional but omitting it
means the refusal cannot tell you what to run:
// @requires-bin: jq | brew install jqerror: mcp-example requires "jq" on PATH.
brew install jq
then run this again — nothing was written.
@pins: <name> <version> records a third-party artifact the preset
installs at an exact version — the npm package behind an mcp command,
a plugin spec, a @fetched jar. Optional and repeatable. It's shown on
the install confirmation and in list -l, so you can see what a preset
drags in before saying yes:
// @pins: @playwright/mcp 0.0.79The version string must also appear in the body or @fetch line it
describes; a test enforces that, so a bump can't land on one side only.
See the existing presets/*.conf for working examples.