summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/duplicate-issues.yml50
-rw-r--r--.github/workflows/guidelines-check.yml49
-rw-r--r--.opencode/agent/github.md13
-rw-r--r--opencode.json3
-rw-r--r--packages/opencode/src/agent/agent.ts57
-rw-r--r--packages/opencode/src/config/config.ts13
-rw-r--r--packages/opencode/src/index.ts3
-rw-r--r--packages/opencode/src/permission/index.ts2
-rw-r--r--packages/opencode/src/session/index.ts4
-rw-r--r--packages/opencode/src/tool/bash.ts27
-rw-r--r--packages/opencode/src/tool/edit.ts8
-rw-r--r--packages/opencode/src/tool/registry.ts15
-rw-r--r--packages/opencode/src/tool/tool.ts1
-rw-r--r--packages/opencode/src/tool/write.ts6
-rw-r--r--packages/opencode/test/tool/bash.test.ts3
-rw-r--r--packages/opencode/test/tool/tool.test.ts1
-rw-r--r--packages/web/src/content/docs/docs/agents.mdx141
-rw-r--r--packages/web/src/content/docs/docs/permissions.mdx2
18 files changed, 344 insertions, 54 deletions
diff --git a/.github/workflows/duplicate-issues.yml b/.github/workflows/duplicate-issues.yml
index e69de29bb..ba9577dbb 100644
--- a/.github/workflows/duplicate-issues.yml
+++ b/.github/workflows/duplicate-issues.yml
@@ -0,0 +1,50 @@
+name: Duplicate Issue Detection
+
+on:
+ issues:
+ types: [opened]
+
+jobs:
+ check-duplicates:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ issues: write
+ id-token: write
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Install opencode
+ run: curl -fsSL https://opencode.ai/install | bash
+
+ - name: Check for duplicate issues
+ env:
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ opencode run --agent github -m anthropic/claude-sonnet-4-20250514 "A new issue has been created: '${{ github.event.issue.title }}'
+
+ Issue body:
+ ${{ github.event.issue.body }}
+
+ Please search through existing issues in this repository to find any potential duplicates of this new issue. Consider:
+ 1. Similar titles or descriptions
+ 2. Same error messages or symptoms
+ 3. Related functionality or components
+ 4. Similar feature requests
+
+ If you find any potential duplicates, please comment on the new issue with:
+ - A brief explanation of why it might be a duplicate
+ - Links to the potentially duplicate issues
+ - A suggestion to check those issues first
+
+ Use this format for the comment:
+ '👋 This issue might be a duplicate of existing issues. Please check:
+ - #[issue_number]: [brief description of similarity]
+
+ If none of these address your specific case, please let us know how this issue differs.'
+
+ If no clear duplicates are found, do not comment."
diff --git a/.github/workflows/guidelines-check.yml b/.github/workflows/guidelines-check.yml
index e69de29bb..9f4915f58 100644
--- a/.github/workflows/guidelines-check.yml
+++ b/.github/workflows/guidelines-check.yml
@@ -0,0 +1,49 @@
+name: Guidelines Check
+
+on:
+ pull_request:
+ types: [opened, synchronize]
+
+jobs:
+ check-guidelines:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
+ id-token: write
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Install opencode
+ run: curl -fsSL https://opencode.ai/install | bash
+
+ - name: Check PR guidelines compliance
+ env:
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ opencode run --agent github -m anthropic/claude-sonnet-4-20250514 "A new pull request has been created: '${{ github.event.pull_request.title }}'
+
+ PR description:
+ ${{ github.event.pull_request.body }}
+
+ Please check all the code changes in this pull request against the guidelines in AGENTS.md file in this repository.
+
+ For each violation you find, create a file comment using the gh CLI. Use this exact format for each violation:
+
+ \`\`\`bash
+ gh pr review ${{ github.event.pull_request.number }} --comment-body 'This violates the AGENTS.md guideline: [specific rule]. Consider: [suggestion]' --file 'path/to/file.ts' --line [line_number]
+ \`\`\`
+
+ When possible, also submit code change suggestions using:
+
+ \`\`\`bash
+ gh pr review ${{ github.event.pull_request.number }} --comment-body 'Suggested fix for AGENTS.md guideline violation:' --file 'path/to/file.ts' --line [line_number] --body '```suggestion
+ [corrected code here]
+ ```'
+ \`\`\`
+
+ Only create comments for actual violations. If the code follows all guidelines, don't run any gh commands."
diff --git a/.opencode/agent/github.md b/.opencode/agent/github.md
new file mode 100644
index 000000000..da3aa4518
--- /dev/null
+++ b/.opencode/agent/github.md
@@ -0,0 +1,13 @@
+---
+permission:
+ bash:
+ "*": "deny"
+ "gh*": "allow"
+mode: subagent
+---
+
+You are running in github actions, typically to evaluate a PR. Do not do
+anything that is outside the scope of that. You have access to the bash tool but
+you can only run `gh` cli commands with it.
+
+Diffs are important but be sure to read the whole file to get the full context.
diff --git a/opencode.json b/opencode.json
index 003253ee7..59f14ac75 100644
--- a/opencode.json
+++ b/opencode.json
@@ -1,8 +1,5 @@
{
"$schema": "https://opencode.ai/config.json",
- "agent": {
- "build": {}
- },
"mcp": {
"context7": {
"type": "remote",
diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts
index b363e0e9b..aa9eeec8e 100644
--- a/packages/opencode/src/agent/agent.ts
+++ b/packages/opencode/src/agent/agent.ts
@@ -5,6 +5,7 @@ import { Provider } from "../provider/provider"
import { generateObject, type ModelMessage } from "ai"
import PROMPT_GENERATE from "./generate.txt"
import { SystemPrompt } from "../session/system"
+import { mergeDeep } from "remeda"
export namespace Agent {
export const Info = z
@@ -14,6 +15,11 @@ export namespace Agent {
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]),
topP: z.number().optional(),
temperature: z.number().optional(),
+ permission: z.object({
+ edit: Config.Permission,
+ bash: z.record(z.string(), Config.Permission),
+ webfetch: Config.Permission.optional(),
+ }),
model: z
.object({
modelID: z.string(),
@@ -31,6 +37,13 @@ export namespace Agent {
const state = App.state("agent", async () => {
const cfg = await Config.get()
+ const defaultPermission: Info["permission"] = {
+ edit: "allow",
+ bash: {
+ "*": "allow",
+ },
+ webfetch: "allow",
+ }
const result: Record<string, Info> = {
general: {
name: "general",
@@ -41,17 +54,20 @@ export namespace Agent {
todowrite: false,
},
options: {},
+ permission: defaultPermission,
mode: "subagent",
},
build: {
name: "build",
tools: {},
options: {},
+ permission: defaultPermission,
mode: "primary",
},
plan: {
name: "plan",
options: {},
+ permission: defaultPermission,
tools: {
write: false,
edit: false,
@@ -70,25 +86,48 @@ export namespace Agent {
item = result[key] = {
name: key,
mode: "all",
+ permission: defaultPermission,
options: {},
tools: {},
}
- const { model, prompt, tools, description, temperature, top_p, mode, ...extra } = value
+ const { model, prompt, tools, description, temperature, top_p, mode, permission, ...extra } = value
item.options = {
...item.options,
...extra,
}
- if (value.model) item.model = Provider.parseModel(value.model)
- if (value.prompt) item.prompt = value.prompt
- if (value.tools)
+ if (model) item.model = Provider.parseModel(model)
+ if (prompt) item.prompt = prompt
+ if (tools)
item.tools = {
...item.tools,
- ...value.tools,
+ ...tools,
}
- if (value.description) item.description = value.description
- if (value.temperature != undefined) item.temperature = value.temperature
- if (value.top_p != undefined) item.topP = value.top_p
- if (value.mode) item.mode = value.mode
+ if (description) item.description = description
+ if (temperature != undefined) item.temperature = temperature
+ if (top_p != undefined) item.topP = top_p
+ if (mode) item.mode = mode
+
+ if (permission ?? cfg.permission) {
+ const merged = mergeDeep(cfg.permission ?? {}, permission ?? {})
+ if (merged.edit) item.permission.edit = merged.edit
+ if (merged.webfetch) item.permission.webfetch = merged.webfetch
+ if (merged.bash) {
+ if (typeof merged.bash === "string") {
+ item.permission.bash = {
+ "*": merged.bash,
+ }
+ }
+ // if granular permissions are provided, default to "ask"
+ if (typeof merged.bash === "object") {
+ item.permission.bash = mergeDeep(
+ {
+ "*": "ask",
+ },
+ merged.bash,
+ )
+ }
+ }
+ }
}
return result
})
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 40ec22b57..a8d84d8bc 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -164,6 +164,9 @@ export namespace Config {
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
export type Mcp = z.infer<typeof Mcp>
+ export const Permission = z.union([z.literal("ask"), z.literal("allow"), z.literal("deny")])
+ export type Permission = z.infer<typeof Permission>
+
export const Agent = z
.object({
model: z.string().optional(),
@@ -174,6 +177,13 @@ export namespace Config {
disable: z.boolean().optional(),
description: z.string().optional().describe("Description of when to use the agent"),
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]).optional(),
+ permission: z
+ .object({
+ edit: Permission.optional(),
+ bash: z.union([Permission, z.record(z.string(), Permission)]).optional(),
+ webfetch: Permission.optional(),
+ })
+ .optional(),
})
.catchall(z.any())
.openapi({
@@ -243,9 +253,6 @@ export namespace Config {
})
export type Layout = z.infer<typeof Layout>
- export const Permission = z.union([z.literal("ask"), z.literal("allow"), z.literal("deny")])
- export type Permission = z.infer<typeof Permission>
-
export const Info = z
.object({
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts
index 007de6509..868c96ba0 100644
--- a/packages/opencode/src/index.ts
+++ b/packages/opencode/src/index.ts
@@ -21,6 +21,9 @@ import { GithubCommand } from "./cli/cmd/github"
const cancel = new AbortController()
+try {
+} catch (e) {}
+
process.on("unhandledRejection", (e) => {
Log.Default.error("rejection", {
e: e instanceof Error ? e.message : e,
diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts
index 53c49696e..d724e9932 100644
--- a/packages/opencode/src/permission/index.ts
+++ b/packages/opencode/src/permission/index.ts
@@ -155,7 +155,7 @@ export namespace Permission {
public readonly permissionID: string,
public readonly toolCallID?: string,
) {
- super(`The user rejected permission to use this functionality`)
+ super(`The user rejected permission to use this specific tool call. You may try again with different parameters.`)
}
}
}
diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts
index 6d2b94863..b881161a2 100644
--- a/packages/opencode/src/session/index.ts
+++ b/packages/opencode/src/session/index.ts
@@ -523,6 +523,7 @@ export namespace Session {
t.execute(args, {
sessionID: input.sessionID,
abort: new AbortController().signal,
+ agent: agent.name,
messageID: userMsg.id,
metadata: async () => {},
}),
@@ -765,7 +766,7 @@ export namespace Session {
const enabledTools = pipe(
agent.tools,
- mergeDeep(await ToolRegistry.enabled(input.providerID, input.modelID)),
+ mergeDeep(await ToolRegistry.enabled(input.providerID, input.modelID, agent)),
mergeDeep(input.tools ?? {}),
)
for (const item of await ToolRegistry.tools(input.providerID, input.modelID)) {
@@ -791,6 +792,7 @@ export namespace Session {
abort: options.abortSignal!,
messageID: assistantMsg.id,
callID: options.toolCallId,
+ agent: agent.name,
metadata: async (val) => {
const match = processor.partFromToolCall(options.toolCallId)
if (match && match.state.status === "running") {
diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts
index f6368ea14..b6266dc22 100644
--- a/packages/opencode/src/tool/bash.ts
+++ b/packages/opencode/src/tool/bash.ts
@@ -5,12 +5,12 @@ import { Tool } from "./tool"
import DESCRIPTION from "./bash.txt"
import { App } from "../app/app"
import { Permission } from "../permission"
-import { Config } from "../config/config"
import { Filesystem } from "../util/filesystem"
import { lazy } from "../util/lazy"
import { Log } from "../util/log"
import { Wildcard } from "../util/wildcard"
import { $ } from "bun"
+import { Agent } from "../agent/agent"
const MAX_OUTPUT_LENGTH = 30000
const DEFAULT_TIMEOUT = 1 * 60 * 1000
@@ -40,20 +40,8 @@ export const BashTool = Tool.define("bash", {
async execute(params, ctx) {
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
const app = App.info()
- const cfg = await Config.get()
const tree = await parser().then((p) => p.parse(params.command))
- const permissions = (() => {
- const value = cfg.permission?.bash
- if (!value)
- return {
- "*": "allow",
- }
- if (typeof value === "string")
- return {
- "*": value,
- }
- return value
- })()
+ const permissions = await Agent.get(ctx.agent).then((x) => x.permission.bash)
let needsAsk = false
for (const node of tree.rootNode.descendantsOfType("command")) {
@@ -93,17 +81,10 @@ export const BashTool = Tool.define("bash", {
// always allow cd if it passes above check
if (!needsAsk && command[0] !== "cd") {
- const action = (() => {
- for (const [pattern, value] of Object.entries(permissions)) {
- const match = Wildcard.match(node.text, pattern)
- log.info("checking", { text: node.text.trim(), pattern, match })
- if (match) return value
- }
- return "ask"
- })()
+ const action = Wildcard.all(node.text, permissions)
if (action === "deny") {
throw new Error(
- "The user has specifically restricted access to this command, you are not allowed to execute it.",
+ `The user has specifically restricted access to this command, you are not allowed to execute it. Here is the configuration: ${JSON.stringify(permissions)}`,
)
}
if (action === "ask") needsAsk = true
diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts
index 1b31b0aa4..8c3bdc63d 100644
--- a/packages/opencode/src/tool/edit.ts
+++ b/packages/opencode/src/tool/edit.ts
@@ -14,8 +14,8 @@ import { App } from "../app/app"
import { File } from "../file"
import { Bus } from "../bus"
import { FileTime } from "../file/time"
-import { Config } from "../config/config"
import { Filesystem } from "../util/filesystem"
+import { Agent } from "../agent/agent"
export const EditTool = Tool.define("edit", {
description: DESCRIPTION,
@@ -40,7 +40,7 @@ export const EditTool = Tool.define("edit", {
throw new Error(`File ${filePath} is not in the current working directory`)
}
- const cfg = await Config.get()
+ const agent = await Agent.get(ctx.agent)
let diff = ""
let contentOld = ""
let contentNew = ""
@@ -48,7 +48,7 @@ export const EditTool = Tool.define("edit", {
if (params.oldString === "") {
contentNew = params.newString
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
- if (cfg.permission?.edit === "ask") {
+ if (agent.permission.edit === "ask") {
await Permission.ask({
type: "edit",
sessionID: ctx.sessionID,
@@ -77,7 +77,7 @@ export const EditTool = Tool.define("edit", {
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
- if (cfg.permission?.edit === "ask") {
+ if (agent.permission.edit === "ask") {
await Permission.ask({
type: "edit",
sessionID: ctx.sessionID,
diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts
index c2fe5943b..e79380579 100644
--- a/packages/opencode/src/tool/registry.ts
+++ b/packages/opencode/src/tool/registry.ts
@@ -11,7 +11,7 @@ import { TodoWriteTool, TodoReadTool } from "./todo"
import { WebFetchTool } from "./webfetch"
import { WriteTool } from "./write"
import { InvalidTool } from "./invalid"
-import { Config } from "../config/config"
+import type { Agent } from "../agent/agent"
export namespace ToolRegistry {
const ALL = [
@@ -66,20 +66,23 @@ export namespace ToolRegistry {
return result
}
- export async function enabled(_providerID: string, _modelID: string): Promise<Record<string, boolean>> {
- const cfg = await Config.get()
+ export async function enabled(
+ _providerID: string,
+ _modelID: string,
+ agent: Agent.Info,
+ ): Promise<Record<string, boolean>> {
const result: Record<string, boolean> = {}
result["patch"] = false
- if (cfg.permission?.edit === "deny") {
+ if (agent.permission.edit === "deny") {
result["edit"] = false
result["patch"] = false
result["write"] = false
}
- if (cfg?.permission?.bash === "deny") {
+ if (agent.permission.bash["*"] === "deny" && Object.keys(agent.permission.bash).length === 1) {
result["bash"] = false
}
- if (cfg?.permission?.webfetch === "deny") {
+ if (agent.permission.webfetch === "deny") {
result["webfetch"] = false
}
diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts
index 1c71b9a77..8be3d0cde 100644
--- a/packages/opencode/src/tool/tool.ts
+++ b/packages/opencode/src/tool/tool.ts
@@ -7,6 +7,7 @@ export namespace Tool {
export type Context<M extends Metadata = Metadata> = {
sessionID: string
messageID: string
+ agent: string
callID?: string
abort: AbortSignal
metadata(input: { title?: string; metadata?: M }): void
diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts
index 5b0028f84..951ad730e 100644
--- a/packages/opencode/src/tool/write.ts
+++ b/packages/opencode/src/tool/write.ts
@@ -8,8 +8,8 @@ import { App } from "../app/app"
import { Bus } from "../bus"
import { File } from "../file"
import { FileTime } from "../file/time"
-import { Config } from "../config/config"
import { Filesystem } from "../util/filesystem"
+import { Agent } from "../agent/agent"
export const WriteTool = Tool.define("write", {
description: DESCRIPTION,
@@ -28,8 +28,8 @@ export const WriteTool = Tool.define("write", {
const exists = await file.exists()
if (exists) await FileTime.assert(ctx.sessionID, filepath)
- const cfg = await Config.get()
- if (cfg.permission?.edit === "ask")
+ const agent = await Agent.get(ctx.agent)
+ if (agent.permission.edit === "ask")
await Permission.ask({
type: "write",
sessionID: ctx.sessionID,
diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts
index e19949d6a..43277dfa2 100644
--- a/packages/opencode/test/tool/bash.test.ts
+++ b/packages/opencode/test/tool/bash.test.ts
@@ -8,6 +8,7 @@ const ctx = {
sessionID: "test",
messageID: "",
toolCallID: "",
+ agent: "build",
abort: AbortSignal.any([]),
metadata: () => {},
}
@@ -33,7 +34,7 @@ describe("tool.bash", () => {
test("cd ../ should fail outside of project root", async () => {
await App.provide({ cwd: projectRoot }, async () => {
- await expect(
+ expect(
bash.execute(
{
command: "cd ../",
diff --git a/packages/opencode/test/tool/tool.test.ts b/packages/opencode/test/tool/tool.test.ts
index a0f7ce909..313aa4242 100644
--- a/packages/opencode/test/tool/tool.test.ts
+++ b/packages/opencode/test/tool/tool.test.ts
@@ -8,6 +8,7 @@ const ctx = {
sessionID: "test",
messageID: "",
toolCallID: "",
+ agent: "build",
abort: AbortSignal.any([]),
metadata: () => {},
}
diff --git a/packages/web/src/content/docs/docs/agents.mdx b/packages/web/src/content/docs/docs/agents.mdx
index bcb0eca2a..beb1b29ae 100644
--- a/packages/web/src/content/docs/docs/agents.mdx
+++ b/packages/web/src/content/docs/docs/agents.mdx
@@ -358,6 +358,147 @@ Here are all the tools can be controlled through the agent config.
---
+### Permissions
+
+Permissions control what actions an agent can take.
+
+- edit, bash, webfetch
+
+Each permission can be set to allow, ask, or deny.
+
+- allow, ask, deny
+
+Configure permissions globally in opencode.json.
+
+```json title="opencode.json"
+{
+ "$schema": "https://opencode.ai/config.json",
+ "permission": {
+ "edit": "ask",
+ "bash": "allow",
+ "webfetch": "deny"
+ }
+}
+```
+
+You can override permissions per agent in JSON.
+
+```json title="opencode.json" {7-18}
+{
+ "$schema": "https://opencode.ai/config.json",
+ "agent": {
+ "build": {
+ "permission": {
+ "edit": "allow",
+ "bash": {
+ "*": "allow",
+ "git push": "ask",
+ "terraform *": "deny"
+ },
+ "webfetch": "ask"
+ }
+ }
+ }
+}
+```
+
+You can also set permissions in Markdown agents.
+
+```markdown title="~/.config/opencode/agent/review.md"
+---
+description: Code review without edits
+mode: subagent
+permission:
+ edit: deny
+ bash: ask
+ webfetch: deny
+---
+
+Only analyze code and suggest changes.
+```
+
+Bash permissions support granular patterns for fine-grained control.
+
+```json title="Allow most, ask for risky, deny terraform"
+{
+ "$schema": "https://opencode.ai/config.json",
+ "permission": {
+ "bash": {
+ "*": "allow",
+ "git push": "ask",
+ "terraform *": "deny"
+ }
+ }
+}
+```
+
+If you provide a granular bash map, the default becomes ask unless you set \* explicitly.
+
+```json title="Granular defaults to ask"
+{
+ "$schema": "https://opencode.ai/config.json",
+ "permission": {
+ "bash": {
+ "git status": "allow"
+ }
+ }
+}
+```
+
+Agent-level permissions merge over global settings.
+
+- Global sets defaults; agent overrides when specified
+
+Specific bash rules can override a global default.
+
+```json title="Global ask, agent allows safe commands"
+{
+ "$schema": "https://opencode.ai/config.json",
+ "permission": { "bash": "ask" },
+ "agent": {
+ "build": {
+ "permission": {
+ "bash": { "git status": "allow", "*": "ask" }
+ }
+ }
+ }
+}
+```
+
+Permissions affect tool availability and prompts differently.
+
+- deny hides tools (edit also hides write/patch); ask prompts; allow runs
+
+For quick reference, here are common setups.
+
+```json title="Read-only reviewer"
+{
+ "$schema": "https://opencode.ai/config.json",
+ "agent": {
+ "review": {
+ "permission": { "edit": "deny", "bash": "deny", "webfetch": "allow" }
+ }
+ }
+}
+```
+
+```json title="Planning agent that can browse but cannot change code"
+{
+ "$schema": "https://opencode.ai/config.json",
+ "agent": {
+ "plan": {
+ "permission": { "edit": "deny", "bash": "deny", "webfetch": "ask" }
+ }
+ }
+}
+```
+
+See the full permissions guide for more patterns.
+
+- /docs/permissions
+
+---
+
### Mode
Control the agent's mode with the `mode` config. The `mode` option is used to determine how the agent can be used.
diff --git a/packages/web/src/content/docs/docs/permissions.mdx b/packages/web/src/content/docs/docs/permissions.mdx
index 2ac7b58a1..44dbc92ef 100644
--- a/packages/web/src/content/docs/docs/permissions.mdx
+++ b/packages/web/src/content/docs/docs/permissions.mdx
@@ -21,6 +21,8 @@ Permissions are configured in your `opencode.json` file under the `permission` k
| `bash` | Control bash command execution |
| `webfetch` | Control web content fetching |
+They can also be configured per agent, see [Agent Configuration](/docs/agents#agent-configuration) for more details.
+
---
### edit