diff --git a/packages/web/src/content/docs/agents.mdx b/packages/web/src/content/docs/agents.mdx
index 1922fece77f..456069cd21d 100644
--- a/packages/web/src/content/docs/agents.mdx
+++ b/packages/web/src/content/docs/agents.mdx
@@ -384,7 +384,7 @@ You can also use wildcards to control multiple tools at once. For example, to di
### Permissions
-You can configure permissions to manage what actions an agent can take. Currently, the permissions for the `edit`, `bash`, and `webfetch` tools can be configured to:
+You can configure permissions to manage what actions an agent can take. Most built-in tools support permissions (for example, `edit`, `bash`, `read`, `webfetch`, `glob`, `grep`, `task`, etc.), plus special gates like `external_directory` and `doom_loop`.
- `"ask"` — Prompt for approval before running the tool
- `"allow"` — Allow all operations without approval
diff --git a/packages/web/src/content/docs/permissions.mdx b/packages/web/src/content/docs/permissions.mdx
index 754a6c875dd..0c878cc507f 100644
--- a/packages/web/src/content/docs/permissions.mdx
+++ b/packages/web/src/content/docs/permissions.mdx
@@ -11,6 +11,7 @@ By default, OpenCode allows most operations without approval, except `doom_loop`
"permission": {
"edit": "allow",
"bash": "ask",
+ "read": "allow",
"skill": "ask",
"webfetch": "deny",
"doom_loop": "ask",
@@ -19,7 +20,7 @@ By default, OpenCode allows most operations without approval, except `doom_loop`
}
```
-This lets you configure granular controls for the `edit`, `bash`, `skill`, `webfetch`, `doom_loop`, and `external_directory` tools.
+This lets you configure granular controls for most built-in tools (and many plugin tools). Note that `write`, `patch`, and `multiedit` are all covered by `permission.edit`.
- `"ask"` — Prompt for approval before running the tool
- `"allow"` — Allow all operations without approval
@@ -29,7 +30,26 @@ This lets you configure granular controls for the `edit`, `bash`, `skill`, `webf
## Tools
-Currently, the permissions for the `edit`, `bash`, `skill`, `webfetch`, `doom_loop`, and `external_directory` tools can be configured through the `permission` option.
+Any tool that might access your filesystem, run commands, or use the network can be gated behind permissions.
+
+Common permission keys:
+
+- `edit` (also covers `write`, `patch`, `multiedit`)
+- `bash`
+- `read`
+- `list`
+- `glob`
+- `grep`
+- `lsp`
+- `task`
+- `todowrite` / `todoread`
+- `skill`
+- `webfetch`
+- `websearch` / `codesearch`
+- `doom_loop`
+- `external_directory`
+
+Each permission key can be either a single action (string), or an object mapping patterns to actions.
---
@@ -135,13 +155,38 @@ The wildcard uses simple regex globbing patterns.
#### Scope of the `"ask"` option
-When the agent asks for permission to run a particular bash command, it will
-request feedback with the three options "accept", "accept always" and "deny".
-The "accept always" answer applies for the rest of the current session.
+When the agent asks for permission to run a particular bash command, it will request feedback with three options: `once`, `always`, and `reject`.
+
+The `always` option stores a rule for a normalized **command prefix** (for example, `git status*` or `npm run dev*`). The prefix is derived from a built-in command-arity table (flags don’t count); for unknown commands it falls back to the first token.
+
+When an agent asks for permission to run a command in a pipeline, each command is parsed separately, and the saved `always` rule applies per command.
+
+---
+
+### read
+
+Use the `permission.read` key to control whether file reads require approval.
+
+The permission patterns for `read` are absolute file paths.
+
+```json title="opencode.json" {4}
+{
+ "$schema": "https://opencode.ai/config.json",
+ "permission": {
+ "read": "ask"
+ }
+}
+```
+
+---
+
+### list, glob, grep
-In addition, command permissions are applied to the first two elements of a command. So, an "accept always" response for a command like `git log` would whitelist `git log *` but not `git commit ...`.
+You can also gate other "read-only" tools:
-When an agent asks for permission to run a command in a pipeline, we use tree sitter to parse each command in the pipeline. The "accept always" permission thus applies separately to each command in the pipeline.
+- `permission.list` — directory listings (patterns are directory paths)
+- `permission.glob` — file globs (patterns are the glob patterns)
+- `permission.grep` — content search (patterns are the regex patterns)
---
diff --git a/packages/web/src/content/docs/plugins.mdx b/packages/web/src/content/docs/plugins.mdx
index 59a7010833d..d297213853f 100644
--- a/packages/web/src/content/docs/plugins.mdx
+++ b/packages/web/src/content/docs/plugins.mdx
@@ -170,8 +170,8 @@ Plugins can subscribe to events as seen below in the Examples section. Here is a
#### Permission Events
+- `permission.asked`
- `permission.replied`
-- `permission.updated`
#### Server Events
diff --git a/packages/web/src/content/docs/sdk.mdx b/packages/web/src/content/docs/sdk.mdx
index 5fe738407c9..2bebd0bf5e9 100644
--- a/packages/web/src/content/docs/sdk.mdx
+++ b/packages/web/src/content/docs/sdk.mdx
@@ -226,27 +226,36 @@ const { providers, default: defaults } = await client.config.providers()
### Sessions
-| Method | Description | Notes |
-| ---------------------------------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
-| `session.list()` | List sessions | Returns Session[] |
-| `session.get({ path })` | Get session | Returns Session |
-| `session.children({ path })` | List child sessions | Returns Session[] |
-| `session.create({ body })` | Create session | Returns Session |
-| `session.delete({ path })` | Delete session | Returns `boolean` |
-| `session.update({ path, body })` | Update session properties | Returns Session |
-| `session.init({ path, body })` | Analyze app and create `AGENTS.md` | Returns `boolean` |
-| `session.abort({ path })` | Abort a running session | Returns `boolean` |
-| `session.share({ path })` | Share session | Returns Session |
-| `session.unshare({ path })` | Unshare session | Returns Session |
-| `session.summarize({ path, body })` | Summarize session | Returns `boolean` |
-| `session.messages({ path })` | List messages in a session | Returns `{ info: `Message`, parts: `Part[]`}[]` |
-| `session.message({ path })` | Get message details | Returns `{ info: `Message`, parts: `Part[]`}` |
-| `session.prompt({ path, body })` | Send prompt message | `body.noReply: true` returns UserMessage (context only). Default returns AssistantMessage with AI response |
-| `session.command({ path, body })` | Send command to session | Returns `{ info: `AssistantMessage`, parts: `Part[]`}` |
-| `session.shell({ path, body })` | Run a shell command | Returns AssistantMessage |
-| `session.revert({ path, body })` | Revert a message | Returns Session |
-| `session.unrevert({ path })` | Restore reverted messages | Returns Session |
-| `postSessionByIdPermissionsByPermissionId({ path, body })` | Respond to a permission request | Returns `boolean` |
+| Method | Description | Notes |
+| ----------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
+| `session.list()` | List sessions | Returns Session[] |
+| `session.get({ path })` | Get session | Returns Session |
+| `session.children({ path })` | List child sessions | Returns Session[] |
+| `session.create({ body })` | Create session | Returns Session |
+| `session.delete({ path })` | Delete session | Returns `boolean` |
+| `session.update({ path, body })` | Update session properties | Returns Session |
+| `session.init({ path, body })` | Analyze app and create `AGENTS.md` | Returns `boolean` |
+| `session.abort({ path })` | Abort a running session | Returns `boolean` |
+| `session.share({ path })` | Share session | Returns Session |
+| `session.unshare({ path })` | Unshare session | Returns Session |
+| `session.summarize({ path, body })` | Summarize session | Returns `boolean` |
+| `session.messages({ path })` | List messages in a session | Returns `{ info: `Message`, parts: `Part[]`}[]` |
+| `session.message({ path })` | Get message details | Returns `{ info: `Message`, parts: `Part[]`}` |
+| `session.prompt({ path, body })` | Send prompt message | `body.noReply: true` returns UserMessage (context only). Default returns AssistantMessage with AI response |
+| `session.command({ path, body })` | Send command to session | Returns `{ info: `AssistantMessage`, parts: `Part[]`}` |
+| `session.shell({ path, body })` | Run a shell command | Returns AssistantMessage |
+| `session.revert({ path, body })` | Revert a message | Returns Session |
+| `session.unrevert({ path })` | Restore reverted messages | Returns Session |
+
+---
+
+### Permissions
+
+| Method | Description | Notes |
+| ----------------------------------------------------------- | ------------------------------- | ------------------------------------------------------------------ |
+| `permission.list()` | List pending permissions | Returns PermissionRequest[] |
+| `permission.reply({ requestID, reply })` | Respond to a permission request | `reply` is `"once"`, `"always"`, or `"reject"` (returns `boolean`) |
+| `permission.respond({ sessionID, permissionID, response })` | Respond to a permission request | Deprecated alias (returns `boolean`) |
---
diff --git a/packages/web/src/content/docs/server.mdx b/packages/web/src/content/docs/server.mdx
index a61d7bae157..e842a514d64 100644
--- a/packages/web/src/content/docs/server.mdx
+++ b/packages/web/src/content/docs/server.mdx
@@ -134,26 +134,37 @@ The opencode server exposes the following APIs.
### Sessions
-| Method | Path | Description | Notes |
-| -------- | ---------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------- |
-| `GET` | `/session` | List all sessions | Returns Session[] |
-| `POST` | `/session` | Create a new session | body: `{ parentID?, title? }`, returns Session |
-| `GET` | `/session/status` | Get session status for all sessions | Returns `{ [sessionID: string]: `SessionStatus` }` |
-| `GET` | `/session/:id` | Get session details | Returns Session |
-| `DELETE` | `/session/:id` | Delete a session and all its data | Returns `boolean` |
-| `PATCH` | `/session/:id` | Update session properties | body: `{ title? }`, returns Session |
-| `GET` | `/session/:id/children` | Get a session's child sessions | Returns Session[] |
-| `GET` | `/session/:id/todo` | Get the todo list for a session | Returns Todo[] |
-| `POST` | `/session/:id/init` | Analyze app and create `AGENTS.md` | body: `{ messageID, providerID, modelID }`, returns `boolean` |
-| `POST` | `/session/:id/fork` | Fork an existing session at a message | body: `{ messageID? }`, returns Session |
-| `POST` | `/session/:id/abort` | Abort a running session | Returns `boolean` |
-| `POST` | `/session/:id/share` | Share a session | Returns Session |
-| `DELETE` | `/session/:id/share` | Unshare a session | Returns Session |
-| `GET` | `/session/:id/diff` | Get the diff for this session | query: `messageID?`, returns FileDiff[] |
-| `POST` | `/session/:id/summarize` | Summarize the session | body: `{ providerID, modelID }`, returns `boolean` |
-| `POST` | `/session/:id/revert` | Revert a message | body: `{ messageID, partID? }`, returns `boolean` |
-| `POST` | `/session/:id/unrevert` | Restore all reverted messages | Returns `boolean` |
-| `POST` | `/session/:id/permissions/:permissionID` | Respond to a permission request | body: `{ response, remember? }`, returns `boolean` |
+| Method | Path | Description | Notes |
+| -------- | ------------------------ | ------------------------------------- | ---------------------------------------------------------------------------------- |
+| `GET` | `/session` | List all sessions | Returns Session[] |
+| `POST` | `/session` | Create a new session | body: `{ parentID?, title? }`, returns Session |
+| `GET` | `/session/status` | Get session status for all sessions | Returns `{ [sessionID: string]: `SessionStatus` }` |
+| `GET` | `/session/:id` | Get session details | Returns Session |
+| `DELETE` | `/session/:id` | Delete a session and all its data | Returns `boolean` |
+| `PATCH` | `/session/:id` | Update session properties | body: `{ title? }`, returns Session |
+| `GET` | `/session/:id/children` | Get a session's child sessions | Returns Session[] |
+| `GET` | `/session/:id/todo` | Get the todo list for a session | Returns Todo[] |
+| `POST` | `/session/:id/init` | Analyze app and create `AGENTS.md` | body: `{ messageID, providerID, modelID }`, returns `boolean` |
+| `POST` | `/session/:id/fork` | Fork an existing session at a message | body: `{ messageID? }`, returns Session |
+| `POST` | `/session/:id/abort` | Abort a running session | Returns `boolean` |
+| `POST` | `/session/:id/share` | Share a session | Returns Session |
+| `DELETE` | `/session/:id/share` | Unshare a session | Returns Session |
+| `GET` | `/session/:id/diff` | Get the diff for this session | query: `messageID?`, returns FileDiff[] |
+| `POST` | `/session/:id/summarize` | Summarize the session | body: `{ providerID, modelID }`, returns `boolean` |
+| `POST` | `/session/:id/revert` | Revert a message | body: `{ messageID, partID? }`, returns `boolean` |
+| `POST` | `/session/:id/unrevert` | Restore all reverted messages | Returns `boolean` |
+
+---
+
+### Permissions
+
+| Method | Path | Description | Notes |
+| ------ | ---------------------------------------- | -------------------------------------------- | --------------------------------------------------------------- |
+| `GET` | `/permission` | List pending permissions | Returns PermissionRequest[] |
+| `POST` | `/permission/:requestID/reply` | Respond to a permission request | body: `{ reply }`, returns `boolean` |
+| `POST` | `/session/:id/permissions/:permissionID` | Respond to a permission request (deprecated) | body: `{ response }`, returns `boolean` |
+
+`reply` / `response` can be one of: `"once"`, `"always"`, or `"reject"`.
---