Policies
Policies decide whether OpenCode may perform an action on a named resource. They are
authored under policies and can also be delivered by a connected
OpenCode Console workspace.
Policies are separate from permissions. Permissions are per-agent tool
rules that can allow, deny, or ask. Policies are binary, never prompt, and only
ever tighten what permissions and providers would otherwise allow.
Configure
Deny one provider everywhere by adding a statement to opencode.jsonc:
{
"$schema": "https://opencode.ai/config.json",
"policies": [{ "action": "provider.use", "resource": "openai", "effect": "deny" }],
}
A denied provider disappears from the catalog and model selection even when it has valid credentials.
Earlier releases read policies from experimental.policies. That key still works and
merges into policies:
{
"experimental": {
"policies": [{ "action": "provider.use", "resource": "openai", "effect": "deny" }],
},
}
Statements
Each statement has three fields:
| Field | Values | Meaning |
|---|---|---|
action | provider.use, tool.use, integration.connect, integration.use, mcp.use, skill.use, plugin.use | The operation being controlled |
resource | string or wildcard pattern | What the statement applies to; depends on the action |
effect | allow, deny | The decision when this statement matches |
Statements that fail validation are dropped with a warning in the server log and
the rest of the list still applies. Check the log after editing a deny.
Matching
Resources use the same whole-value wildcards as permissions: * matches zero or
more characters and ? matches one.
{
"policies": [{ "action": "provider.use", "resource": "company-*", "effect": "deny" }],
}
This denies company-us and company-eu. A pattern ending in * also matches the
value without arguments, so shell:git push * covers git push on its own.
Order
When several statements match, the last one wins. There is no specificity rule, so put broad statements first and exceptions after them. If nothing matches, the action is allowed.
{
"policies": [
{ "action": "provider.use", "resource": "*", "effect": "deny" },
{ "action": "provider.use", "resource": "anthropic", "effect": "allow" },
],
}
Only Anthropic remains available.
Providers
provider.use controls whether a provider is usable at all. The resource is the
provider ID: anthropic, openai, opencode, or a custom ID from
providers.
{
"providers": {
"company-ai": {
"package": "@opencode/ai/providers/openai-compatible",
"settings": { "baseURL": "https://ai.company.example/v1" },
},
},
"policies": [
{ "action": "provider.use", "resource": "*", "effect": "deny" },
{ "action": "provider.use", "resource": "company-ai", "effect": "allow" },
],
}
The providers entry configures the endpoint; the statements make it the only
provider OpenCode will use. Policy applies however a provider became known: catalog
data, environment variables, saved accounts, built-in plugins, or configuration.
Use provider.use instead of the V1 enabled_providers and disabled_providers
lists. V1 files still load; see
Migrate from V1.
Tools
tool.use statements hard-deny a permission check. The resource is
<action>:<value>, matched against every resource the tool checks.
{
"policies": [
{ "action": "tool.use", "resource": "shell:git push *", "effect": "deny" },
{ "action": "tool.use", "resource": "edit:*.env", "effect": "deny" },
{ "action": "tool.use", "resource": "webfetch:*", "effect": "deny" },
],
}
A denied check fails with Blocked by configuration policy instead of prompting. It
applies after agent rules and saved approvals, so it overrides an ask and an
“Allow always” approval alike.
| Pattern | Blocks |
|---|---|
shell:* | Every shell command |
read:*/.ssh/* | Reading SSH material |
external_directory:* | Any access outside the Location or worktree |
subagent:general | Launching one subagent |
github_delete_repository:* | One MCP tool |
* | Everything not already denied |
A tool.use statement with allow never grants access. It only lifts an earlier
broader deny, after which the agent’s own rules decide.
{
"policies": [
{ "action": "tool.use", "resource": "shell:*", "effect": "deny" },
{ "action": "tool.use", "resource": "shell:git status *", "effect": "allow" },
],
}
git status falls back to the agent’s shell rule; every other command is blocked.
Integrations
An integration is anything you connect in /connect: the OpenCode Console
(opencode), a provider such as anthropic, a web search service such as exa,
or a well-known config origin such as https://ai.acme.com. Signing in to an MCP
server is not affected by integration policies; use mcp.use to control MCP
servers.
integration.connect controls which integrations /connect lists and which can be
connected. The resource is the integration ID.
integration.use controls everything an integration supplies: its providers and
models, its web search, and the MCP servers and config a Console workspace or
well-known origin delivers. The resource is <integration>#<account>, where the
account is the connected organization or account ID, such as
opencode#org_01M3CAP8H43Z49CER6NT0Y3ZF1. Without a connected account it is the
bare integration ID: the signed-out free tier is opencode, an ANTHROPIC_API_KEY
environment variable is anthropic, and a provider from opencode.json is its
provider ID.
This MDM policy requires one Console workspace and nothing else:
{
"policies": [
{ "action": "integration.connect", "resource": "*", "effect": "deny" },
{ "action": "integration.connect", "resource": "opencode", "effect": "allow" },
{ "action": "integration.use", "resource": "*", "effect": "deny" },
{ "action": "integration.use", "resource": "opencode#org_01M3CAP8H43Z49CER6NT0Y3ZF1", "effect": "allow" },
],
}
/connect offers only the Console so members can sign in. The free tier, personal
Console accounts, other organizations, environment API keys, and providers from
opencode.json are all unavailable. The workspace’s providers, MCP servers, and
web search work, and changing them in the Console needs no new MDM profile.
Only managed policies can withhold the MCP servers and config an integration delivers; its own policies keep applying either way.
A provider the connected Console workspace supplies can’t be changed by user, project, or remote configuration: its settings, headers, models, and package come only from the Console and managed configuration. Other providers stay configurable as usual.
MCP servers, skills, and plugins
| Action | Resource |
|---|---|
mcp.use | An MCP server by its configured name |
skill.use | A skill by its ID |
plugin.use | A plugin by unversioned package name or absolute path |
{
"policies": [
{ "action": "mcp.use", "resource": "*", "effect": "deny" },
{ "action": "mcp.use", "resource": "github", "effect": "allow" },
{ "action": "plugin.use", "resource": "opencode-acme-cli", "effect": "deny" },
],
}
A denied MCP server or skill is removed, and a denied plugin is not loaded, however it was configured.
Upgrading from integration.use
Before OpenCode 2.3.0, integration.use covered MCP servers, skills, and plugins
through an mcp:, skill:, or plugin: prefix. Prefixed statements still load and
are converted to mcp.use, skill.use, or plugin.use.
A statement without one of those prefixes, including a bare *, now applies to
integrations instead. A broad deny such as
{ "action": "integration.use", "resource": "*", "effect": "deny" }
no longer blocks MCP servers, skills, or plugins. Instead it blocks every provider, including the free tier, unless a later statement allows an integration. To keep the old meaning, replace it with:
{
"policies": [
{ "action": "mcp.use", "resource": "*", "effect": "deny" },
{ "action": "skill.use", "resource": "*", "effect": "deny" },
{ "action": "plugin.use", "resource": "*", "effect": "deny" },
],
}
Exceptions move the same way: integration.use mcp:github becomes mcp.use
github, and skill:team:review becomes skill.use team:review.
Clients older than 2.3.0 ignore integration.connect, integration.use (in its new
meaning), mcp.use, skill.use, and plugin.use, so require 2.3.0 or newer
wherever you rely on them.
Precedence
Ordinary settings let the nearest configuration win. Policies reverse that: statements from broader configuration are evaluated later, so they override narrower ones. Managed configuration and the connected Console always outrank user and project files.
| Authority | Source |
|---|---|
| 1 (highest) | macOS managed preferences (/Library/Managed Preferences/ai.opencode.managed.plist) |
| 2 | System opencode.json(c) (/Library/Application Support/opencode, /etc/opencode, %ProgramData%\opencode) |
| 3 | Connected OpenCode Console workspace |
| 4 | Global ~/.config/opencode/opencode.json(c) |
| 5 | Direct opencode.json(c); outer directories beat inner ones |
| 6 (lowest) | .opencode/opencode.json(c); outer directories beat inner ones |
A repository cannot re-enable a provider you deny globally:
{
"policies": [{ "action": "provider.use", "resource": "openai", "effect": "deny" }],
}{
"policies": [{ "action": "provider.use", "resource": "openai", "effect": "allow" }],
}openai stays denied. Within one file, statements keep their written order.
Console
A workspace on the OpenCode Console compiles its Providers and Tools
policies into regular policies statements in the workspace configuration. OpenCode
merges that configuration like a configuration file that ranks right below managed
preferences and the system file, so an organization deny cannot be lifted by a
repository or user allow, and an organization allow lifts a lower-authority deny.
Managed preferences and the system file still override the workspace.
Denials from these statements name the workspace:
Blocked by Acme's policy
Statements follow the connected account.
| Event | Result |
|---|---|
| Console policy changes | Applied on the next fetch, about once a minute |
| Switch to another workspace | Statements replaced with that workspace’s |
| Disconnect | Statements cleared |
| Console unreachable | Last statements for that connection stay in effect |
The Console plugin and the policy plugin cannot be disabled from plugins; see
plugin control.