Lower-kebab names its loader will not coerce, tools gated by permission key rather than tool name, and no native hook surface, so a hook here is advisory prose. Owner: crates/core/src/harness/opencode.rs.
| Scope | Path | Relocated by |
|---|---|---|
| Global | ~/.config/opencode |
OPENCODE_CONFIG (the file; its parent becomes the root), else OPENCODE_CONFIG_DIR |
| Project | <project>/.opencode, with the config file at the repo root |
nothing |
The config file for a scope is opencode.jsonc when it exists, else opencode.json; at project scope opencode.json wins unless only the .jsonc variant exists. Project markers: .opencode/, opencode.json, opencode.jsonc.
<base> is ~/.config/opencode at global scope and <project>/.opencode at project scope; the layout is the same under each.
| Kind | Path | Caps |
|---|---|---|
| agent | <base>/agents/*.md |
managed, both |
| skill | .agents/skills/<name>/SKILL.md in a project, ~/.agents/skills/<name>/SKILL.md globally; <base>/skills/<name>/SKILL.md for a copy delivery |
managed, both |
| hook | <base>/instructions/kendex-hook-<name>.md |
managed, both, advisory |
| command | <base>/commands/*.md, plus the legacy singular <base>/command/*.md for what is already there |
managed, both |
| mcp-server | config mcp key, jsonc tolerated on read, per-entry enabled |
managed, both |
| plugin | <base>/plugins/*.{js,ts,mjs,cjs}, plus the config plugin array of npm refs |
observe only, both |
| pi-extension | — | unsupported |
The hook surface reads only files whose name starts kendex-hook- (HOOK_INSTRUCTION_MARKER, crates/core/src/harness/opencode.rs).
- Name rule
LowerKebab, at most 64 characters, for agents and skills; capitals and underscores make one unloadable. Namespace separator-. - MCP transports: stdio and streamable HTTP; servers are
local(command) orremote(url), never SSE, and an SSE declaration is refused for this harness with that reason. A command server is written as{"type": "local", "command": [command, args…], "environment": env, "enabled": true}and a url server as{"type": "remote", "url": url, "enabled": true}undermcp.<name>in the scope's config file, anenvvalue's$NAMEor${NAME}reference spelled{env:NAME}, the form OpenCode substitutes (server,crates/core/src/engine/opencode.rs). OpenCode merges its config files field by field, soenabledis always written: switching one off writesenabled: falseand on writesenabled: true, and the declaration stays either way. Other keys in the file are untouched, a file kendex creates carries the$schemaline, and a config holding comments is not edited: the registration reports the conflict instead. Servers are read once at startup. - Command file: markdown installed as
commands/<name>.md. The filename gives the/nametyped. The source bytes stay unchanged when no command instructions apply. See consumer instructions for configured output. OpenCode readsdescription,agent,model,variantandsubtaskfrom the frontmatter and drops any other key, the body is the template, and$ARGUMENTS,$1,!`command`and@fileexpand as they do on Claude Code. Only.mdloads, so the.disabledrename toggle is safe. The command loader keys on the filename alone with no case or character rule, so the lower-kebab rule above binds agents and skills and not commands. Commands are read once at startup. - Agent file: YAML frontmatter and a markdown system prompt. Fields written:
description,mode,model,color(hex only),options.reasoningEffortand its companions, and apermission:map of denies (crates/core/src/render/agent/opencode.rs).modeisprimary,subagentorall; kendex writessubagentby default and spells a source'sallassubagenttoo. - Model dialect: classes omit the model field with a managed-session limitation warning under D021. Exact ids require
provider/modeland emit one compatibility warning. Readback refuses unresolved class tokens and bare ids. An omitted field follows the native session. - Permissions: tools are gated by permission key, every entry translated first (
opencode_permission,crates/core/src/render/vocab/mod.rs); the keys the loader knows areread,edit,glob,grep,bash,task,skill,lsp,question,webfetch. An allowlist is expressed by denying everything else over exactly that set,skillstays allowed, and an entry that maps to no known permission warns. Subagents always denytask, and every agent but arole: plannerdeniesquestion. - Rendered agents are re-read through OpenCode's own rules in plan preview: the frontmatter must parse,
modemust be one of the three, every permission value must beallow,askordeny; a model naming no provider is advisory (crates/core/src/render/validate/agent.rs).
Advisory: a hook installs as an instruction file stating the constraint plus a reference in the config's instructions[] array, and a PreToolUse hook matching Bash additionally sets permission.bash = {"*": "ask"}. The plan preview, the report and the tool's card carry the advisory notice (advisory_notice, crates/core/src/engine/targets.rs). Disabling renames the instruction file to .disabled and removes the config reference. A refresh cuts marker-named rows nothing renders anymore and touches no other row, a person's own file in the instructions directory included (stale_instruction_rows, crates/core/src/engine/stale.rs).
| Scope | Route | Drift and lock |
|---|---|---|
| Global | Unsupported; reported, no style file or block | No installation row |
| Project | Unsupported; reported, no style file or block | No installation row |