Compare commits

..
19 changed files with 384 additions and 140 deletions
+136
View File
@@ -0,0 +1,136 @@
name: PR Standards
on:
pull_request_target:
types: [opened, edited, synchronize]
jobs:
check-standards:
if: |
github.event.pull_request.user.login != 'actions-user' &&
github.event.pull_request.user.login != 'opencode' &&
github.event.pull_request.user.login != 'rekram1-node' &&
github.event.pull_request.user.login != 'thdxr' &&
github.event.pull_request.user.login != 'kommander' &&
github.event.pull_request.user.login != 'jayair' &&
github.event.pull_request.user.login != 'fwang' &&
github.event.pull_request.user.login != 'adamdotdevin' &&
github.event.pull_request.user.login != 'iamdavidhill' &&
github.event.pull_request.user.login != 'opencode-agent[bot]'
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Check PR standards
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
const title = pr.title;
async function addLabel(label) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
labels: [label]
});
}
async function removeLabel(label) {
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
name: label
});
} catch (e) {
// Label wasn't present, ignore
}
}
async function comment(marker, body) {
const markerText = `<!-- pr-standards:${marker} -->`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number
});
const existing = comments.find(c => c.body.includes(markerText));
if (existing) return;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: markerText + '\n' + body
});
}
// Step 1: Check title format
const validPrefixes = ['feat:', 'fix:', 'docs:', 'chore:', 'refactor:', 'test:'];
const hasValidTitle = validPrefixes.some(prefix => title.startsWith(prefix));
if (!hasValidTitle) {
await addLabel('needs:title');
await comment('title', `Hey! Your PR title \`${title}\` doesn't follow conventional commit format.
Please update it to start with one of:
- \`feat:\` new feature
- \`fix:\` bug fix
- \`docs:\` documentation changes
- \`chore:\` maintenance tasks
- \`refactor:\` code refactoring
- \`test:\` adding or updating tests
See [CONTRIBUTING.md](../blob/dev/CONTRIBUTING.md#pr-titles) for details.`);
return;
}
await removeLabel('needs:title');
// Step 2: Check for linked issue (skip for docs/refactor PRs)
const skipIssueCheck = title.startsWith('docs:') || title.startsWith('refactor:');
if (skipIssueCheck) {
await removeLabel('needs:issue');
console.log('Skipping issue check for docs/refactor PR');
return;
}
const query = `
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
closingIssuesReferences(first: 1) {
totalCount
}
}
}
}
`;
const result = await github.graphql(query, {
owner: context.repo.owner,
repo: context.repo.repo,
number: pr.number
});
const linkedIssues = result.repository.pullRequest.closingIssuesReferences.totalCount;
if (linkedIssues === 0) {
await addLabel('needs:issue');
await comment('issue', `Thanks for your contribution!
This PR doesn't have a linked issue. All PRs must reference an existing issue.
Please:
1. Open an issue describing the bug/feature (if one doesn't exist)
2. Add \`Fixes #<number>\` or \`Closes #<number>\` to this PR description
See [CONTRIBUTING.md](../blob/dev/CONTRIBUTING.md#issue-first-policy) for details.`);
return;
}
await removeLabel('needs:issue');
console.log('PR meets all standards');
+48 -4
View File
@@ -149,11 +149,55 @@ With that said, you may want to try these methods, as they might work for you.
## Pull Request Expectations
- Try to keep pull requests small and focused.
- Link relevant issue(s) in the description
### Issue First Policy
**All PRs must reference an existing issue.** Before opening a PR, open an issue describing the bug or feature. This helps maintainers triage and prevents duplicate work. PRs without a linked issue may be closed without review.
- Use `Fixes #123` or `Closes #123` in your PR description to link the issue
- For small fixes, a brief issue is fine - just enough context for maintainers to understand the problem
### General Requirements
- Keep pull requests small and focused
- Explain the issue and why your change fixes it
- Avoid having verbose LLM generated PR descriptions
- Before adding new functions or functionality, ensure that such behavior doesn't already exist elsewhere in the codebase.
- Before adding new functionality, ensure it doesn't already exist elsewhere in the codebase
### UI Changes
If your PR includes UI changes, please include screenshots or videos showing the before and after. This helps maintainers review faster and gives you quicker feedback.
### Logic Changes
For non-UI changes (bug fixes, new features, refactors), explain **how you verified it works**:
- What did you test?
- How can a reviewer reproduce/confirm the fix?
### No AI-Generated Walls of Text
Long, AI-generated PR descriptions and issues are not acceptable and may be ignored. Respect the maintainers' time:
- Write short, focused descriptions
- Explain what changed and why in your own words
- If you can't explain it briefly, your PR might be too large
### PR Titles
PR titles should follow conventional commit standards:
- `feat:` new feature or functionality
- `fix:` bug fix
- `docs:` documentation or README changes
- `chore:` maintenance tasks, dependency updates, etc.
- `refactor:` code refactoring without changing behavior
- `test:` adding or updating tests
Examples:
- `docs: update contributing guidelines`
- `fix: resolve crash on startup`
- `feat: add dark mode support`
- `chore: bump dependency versions`
### Style Preferences
+1
View File
@@ -194,3 +194,4 @@
| 2026-01-05 | 1,738,171 (+65,515) | 1,353,043 (+13,160) | 3,091,214 (+78,675) |
| 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334) | 3,338,365 (+247,151) |
| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) |
| 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) |
+15
View File
@@ -209,6 +209,21 @@ export namespace Agent {
item.options = mergeDeep(item.options, value.options ?? {})
item.permission = PermissionNext.merge(item.permission, PermissionNext.fromConfig(value.permission ?? {}))
}
// Ensure Truncate.DIR is allowed unless explicitly configured
for (const name in result) {
const agent = result[name]
const explicit = agent.permission.some(
(r) => r.permission === "external_directory" && r.pattern === Truncate.DIR && r.action === "deny",
)
if (explicit) continue
result[name].permission = PermissionNext.merge(
result[name].permission,
PermissionNext.fromConfig({ external_directory: { [Truncate.DIR]: "allow" } }),
)
}
return result
})
@@ -6,6 +6,7 @@ import { cmd } from "../cmd"
export const AgentCommand = cmd({
command: "agent <name>",
describe: "show agent configuration details",
builder: (yargs) =>
yargs.positional("name", {
type: "string",
@@ -5,6 +5,7 @@ import { cmd } from "../cmd"
export const ConfigCommand = cmd({
command: "config",
describe: "show resolved configuration",
builder: (yargs) => yargs,
async handler() {
await bootstrap(process.cwd(), async () => {
@@ -6,6 +6,7 @@ import { Ripgrep } from "@/file/ripgrep"
const FileSearchCommand = cmd({
command: "search <query>",
describe: "search files by query",
builder: (yargs) =>
yargs.positional("query", {
type: "string",
@@ -22,6 +23,7 @@ const FileSearchCommand = cmd({
const FileReadCommand = cmd({
command: "read <path>",
describe: "read file contents as JSON",
builder: (yargs) =>
yargs.positional("path", {
type: "string",
@@ -38,6 +40,7 @@ const FileReadCommand = cmd({
const FileStatusCommand = cmd({
command: "status",
describe: "show file status information",
builder: (yargs) => yargs,
async handler() {
await bootstrap(process.cwd(), async () => {
@@ -49,6 +52,7 @@ const FileStatusCommand = cmd({
const FileListCommand = cmd({
command: "list <path>",
describe: "list files in a directory",
builder: (yargs) =>
yargs.positional("path", {
type: "string",
@@ -65,6 +69,7 @@ const FileListCommand = cmd({
const FileTreeCommand = cmd({
command: "tree [dir]",
describe: "show directory tree",
builder: (yargs) =>
yargs.positional("dir", {
type: "string",
@@ -79,6 +84,7 @@ const FileTreeCommand = cmd({
export const FileCommand = cmd({
command: "file",
describe: "file system debugging utilities",
builder: (yargs) =>
yargs
.command(FileReadCommand)
@@ -12,6 +12,7 @@ import { AgentCommand } from "./agent"
export const DebugCommand = cmd({
command: "debug",
describe: "debugging and troubleshooting tools",
builder: (yargs) =>
yargs
.command(ConfigCommand)
@@ -25,6 +26,7 @@ export const DebugCommand = cmd({
.command(PathsCommand)
.command({
command: "wait",
describe: "wait indefinitely (for debugging)",
async handler() {
await bootstrap(process.cwd(), async () => {
await new Promise((resolve) => setTimeout(resolve, 1_000 * 60 * 60 * 24))
@@ -37,6 +39,7 @@ export const DebugCommand = cmd({
const PathsCommand = cmd({
command: "paths",
describe: "show global paths (data, config, cache, state)",
handler() {
for (const [key, value] of Object.entries(Global.Path)) {
console.log(key.padEnd(10), value)
@@ -6,6 +6,7 @@ import { EOL } from "os"
export const LSPCommand = cmd({
command: "lsp",
describe: "LSP debugging utilities",
builder: (yargs) =>
yargs.command(DiagnosticsCommand).command(SymbolsCommand).command(DocumentSymbolsCommand).demandCommand(),
async handler() {},
@@ -13,6 +14,7 @@ export const LSPCommand = cmd({
const DiagnosticsCommand = cmd({
command: "diagnostics <file>",
describe: "get diagnostics for a file",
builder: (yargs) => yargs.positional("file", { type: "string", demandOption: true }),
async handler(args) {
await bootstrap(process.cwd(), async () => {
@@ -25,6 +27,7 @@ const DiagnosticsCommand = cmd({
export const SymbolsCommand = cmd({
command: "symbols <query>",
describe: "search workspace symbols",
builder: (yargs) => yargs.positional("query", { type: "string", demandOption: true }),
async handler(args) {
await bootstrap(process.cwd(), async () => {
@@ -37,6 +40,7 @@ export const SymbolsCommand = cmd({
export const DocumentSymbolsCommand = cmd({
command: "document-symbols <uri>",
describe: "get symbols from a document",
builder: (yargs) => yargs.positional("uri", { type: "string", demandOption: true }),
async handler(args) {
await bootstrap(process.cwd(), async () => {
@@ -6,12 +6,14 @@ import { cmd } from "../cmd"
export const RipgrepCommand = cmd({
command: "rg",
describe: "ripgrep debugging utilities",
builder: (yargs) => yargs.command(TreeCommand).command(FilesCommand).command(SearchCommand).demandCommand(),
async handler() {},
})
const TreeCommand = cmd({
command: "tree",
describe: "show file tree using ripgrep",
builder: (yargs) =>
yargs.option("limit", {
type: "number",
@@ -25,6 +27,7 @@ const TreeCommand = cmd({
const FilesCommand = cmd({
command: "files",
describe: "list files using ripgrep",
builder: (yargs) =>
yargs
.option("query", {
@@ -56,6 +59,7 @@ const FilesCommand = cmd({
const SearchCommand = cmd({
command: "search <pattern>",
describe: "search file contents using ripgrep",
builder: (yargs) =>
yargs
.positional("pattern", {
@@ -5,6 +5,7 @@ import { cmd } from "../cmd"
export const ScrapCommand = cmd({
command: "scrap",
describe: "list all known projects",
builder: (yargs) => yargs,
async handler() {
const timer = Log.Default.time("scrap")
@@ -5,6 +5,7 @@ import { cmd } from "../cmd"
export const SkillCommand = cmd({
command: "skill",
describe: "list all available skills",
builder: (yargs) => yargs,
async handler() {
await bootstrap(process.cwd(), async () => {
@@ -4,12 +4,14 @@ import { cmd } from "../cmd"
export const SnapshotCommand = cmd({
command: "snapshot",
describe: "snapshot debugging utilities",
builder: (yargs) => yargs.command(TrackCommand).command(PatchCommand).command(DiffCommand).demandCommand(),
async handler() {},
})
const TrackCommand = cmd({
command: "track",
describe: "track current snapshot state",
async handler() {
await bootstrap(process.cwd(), async () => {
console.log(await Snapshot.track())
@@ -19,6 +21,7 @@ const TrackCommand = cmd({
const PatchCommand = cmd({
command: "patch <hash>",
describe: "show patch for a snapshot hash",
builder: (yargs) =>
yargs.positional("hash", {
type: "string",
@@ -34,6 +37,7 @@ const PatchCommand = cmd({
const DiffCommand = cmd({
command: "diff <hash>",
describe: "show diff for a snapshot hash",
builder: (yargs) =>
yargs.positional("hash", {
type: "string",
@@ -26,7 +26,7 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
return ready()
},
signal<T>(name: string, defaultValue: T) {
if (!kvStore[name]) setKvStore(name, defaultValue)
if (kvStore[name] === undefined) setKvStore(name, defaultValue)
return [
function () {
return result.get(name)
@@ -1,4 +1,5 @@
import {
batch,
createContext,
createEffect,
createMemo,
@@ -52,7 +53,6 @@ import { useDialog } from "../../ui/dialog"
import { TodoItem } from "../../component/todo-item"
import { DialogMessage } from "./dialog-message"
import type { PromptInfo } from "../../component/prompt/history"
import { iife } from "@/util/iife"
import { DialogConfirm } from "@tui/ui/dialog-confirm"
import { DialogTimeline } from "./dialog-timeline"
import { DialogForkFromTimeline } from "./dialog-fork-from-timeline"
@@ -136,23 +136,25 @@ export function Session() {
})
const dimensions = useTerminalDimensions()
const [sidebar, setSidebar] = createSignal<"show" | "hide" | "auto">(kv.get("sidebar", "auto"))
const [sidebar, setSidebar] = kv.signal<"auto" | "hide">("sidebar", "hide")
const [sidebarOpen, setSidebarOpen] = createSignal(false)
const [conceal, setConceal] = createSignal(true)
const [showThinking, setShowThinking] = createSignal(kv.get("thinking_visibility", true))
const [showTimestamps, setShowTimestamps] = createSignal(kv.get("timestamps", "hide") === "show")
const [showDetails, setShowDetails] = createSignal(kv.get("tool_details_visibility", true))
const [showAssistantMetadata, setShowAssistantMetadata] = createSignal(kv.get("assistant_metadata_visibility", true))
const [showScrollbar, setShowScrollbar] = createSignal(kv.get("scrollbar_visible", false))
const [showThinking, setShowThinking] = kv.signal("thinking_visibility", true)
const [timestamps, setTimestamps] = kv.signal<"hide" | "show">("timestamps", "hide")
const [showDetails, setShowDetails] = kv.signal("tool_details_visibility", true)
const [showAssistantMetadata, setShowAssistantMetadata] = kv.signal("assistant_metadata_visibility", true)
const [showScrollbar, setShowScrollbar] = kv.signal("scrollbar_visible", false)
const [diffWrapMode, setDiffWrapMode] = createSignal<"word" | "none">("word")
const [animationsEnabled, setAnimationsEnabled] = createSignal(kv.get("animations_enabled", true))
const [animationsEnabled, setAnimationsEnabled] = kv.signal("animations_enabled", true)
const wide = createMemo(() => dimensions().width > 120)
const sidebarVisible = createMemo(() => {
if (session()?.parentID) return false
if (sidebar() === "show") return true
if (sidebarOpen()) return true
if (sidebar() === "auto" && wide()) return true
return false
})
const showTimestamps = createMemo(() => timestamps() === "show")
const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() ? 42 : 0) - 4)
const scrollAcceleration = createMemo(() => {
@@ -453,13 +455,11 @@ export function Session() {
keybind: "sidebar_toggle",
category: "Session",
onSelect: (dialog) => {
setSidebar((prev) => {
if (prev === "auto") return sidebarVisible() ? "hide" : "show"
if (prev === "show") return "hide"
return "show"
batch(() => {
const isVisible = sidebarVisible()
setSidebar(() => (isVisible ? "hide" : "auto"))
setSidebarOpen(!isVisible)
})
if (sidebar() === "show") kv.set("sidebar", "auto")
if (sidebar() === "hide") kv.set("sidebar", "hide")
dialog.clear()
},
},
@@ -478,11 +478,7 @@ export function Session() {
value: "session.toggle.timestamps",
category: "Session",
onSelect: (dialog) => {
setShowTimestamps((prev) => {
const next = !prev
kv.set("timestamps", next ? "show" : "hide")
return next
})
setTimestamps((prev) => (prev === "show" ? "hide" : "show"))
dialog.clear()
},
},
@@ -491,11 +487,7 @@ export function Session() {
value: "session.toggle.thinking",
category: "Session",
onSelect: (dialog) => {
setShowThinking((prev) => {
const next = !prev
kv.set("thinking_visibility", next)
return next
})
setShowThinking((prev) => !prev)
dialog.clear()
},
},
@@ -514,9 +506,7 @@ export function Session() {
keybind: "tool_details",
category: "Session",
onSelect: (dialog) => {
const newValue = !showDetails()
setShowDetails(newValue)
kv.set("tool_details_visibility", newValue)
setShowDetails((prev) => !prev)
dialog.clear()
},
},
@@ -526,11 +516,7 @@ export function Session() {
keybind: "scrollbar_toggle",
category: "Session",
onSelect: (dialog) => {
setShowScrollbar((prev) => {
const next = !prev
kv.set("scrollbar_visible", next)
return next
})
setShowScrollbar((prev) => !prev)
dialog.clear()
},
},
@@ -539,11 +525,7 @@ export function Session() {
value: "session.toggle.animations",
category: "Session",
onSelect: (dialog) => {
setAnimationsEnabled((prev) => {
const next = !prev
kv.set("animations_enabled", next)
return next
})
setAnimationsEnabled((prev) => !prev)
dialog.clear()
},
},
@@ -905,7 +887,7 @@ export function Session() {
<box flexDirection="row">
<box flexGrow={1} paddingBottom={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={1}>
<Show when={session()}>
<Show when={!sidebarVisible()}>
<Show when={!sidebarVisible() || !wide()}>
<Header />
</Show>
<scrollbox
@@ -1046,9 +1028,6 @@ export function Session() {
sessionID={route.sessionID}
/>
</box>
<Show when={!sidebarVisible()}>
<Footer />
</Show>
</Show>
<Toast />
</box>
+1 -1
View File
@@ -30,7 +30,7 @@ function getNetworkIPs() {
export const WebCommand = cmd({
command: "web",
builder: (yargs) => withNetworkOptions(yargs),
describe: "starts a headless opencode server",
describe: "start opencode server and open web interface",
handler: async (args) => {
const opts = await resolveNetworkOptions(args)
const server = Server.listen(opts)
+3 -3
View File
@@ -101,9 +101,9 @@ const cli = yargs(hideBin(process.argv))
.command(SessionCommand)
.fail((msg) => {
if (
msg.startsWith("Unknown argument") ||
msg.startsWith("Not enough non-option arguments") ||
msg.startsWith("Invalid values:")
msg?.startsWith("Unknown argument") ||
msg?.startsWith("Not enough non-option arguments") ||
msg?.startsWith("Invalid values:")
) {
cli.showHelp("log")
}
@@ -446,3 +446,66 @@ test("legacy tools config maps write/edit/patch/multiedit to edit permission", a
},
})
})
test("Truncate.DIR is allowed even when user denies external_directory globally", async () => {
const { Truncate } = await import("../../src/tool/truncation")
await using tmp = await tmpdir({
config: {
permission: {
external_directory: "deny",
},
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const build = await Agent.get("build")
expect(PermissionNext.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("allow")
expect(PermissionNext.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny")
},
})
})
test("Truncate.DIR is allowed even when user denies external_directory per-agent", async () => {
const { Truncate } = await import("../../src/tool/truncation")
await using tmp = await tmpdir({
config: {
agent: {
build: {
permission: {
external_directory: "deny",
},
},
},
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const build = await Agent.get("build")
expect(PermissionNext.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("allow")
expect(PermissionNext.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny")
},
})
})
test("explicit Truncate.DIR deny is respected", async () => {
const { Truncate } = await import("../../src/tool/truncation")
await using tmp = await tmpdir({
config: {
permission: {
external_directory: {
"*": "deny",
[Truncate.DIR]: "deny",
},
},
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const build = await Agent.get("build")
expect(PermissionNext.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
},
})
})
+70 -89
View File
@@ -5,86 +5,37 @@ description: Manage the tools an LLM can use.
Tools allow the LLM to perform actions in your codebase. OpenCode comes with a set of built-in tools, but you can extend it with [custom tools](/docs/custom-tools) or [MCP servers](/docs/mcp-servers).
By default, all tools are **enabled** and don't need permission to run. But you can configure this and control the [permissions](/docs/permissions) through your config.
By default, all tools are **enabled** and don't need permission to run. You can control tool behavior through [permissions](/docs/permissions).
---
## Configure
You can configure tools globally or per agent. Agent-specific configs override global settings.
By default, all tools are set to `true`. To disable a tool, set it to `false`.
---
### Global
Disable or enable tools globally using the `tools` option.
Use the `permission` field to control tool behavior. You can allow, deny, or require approval for each tool.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"tools": {
"write": false,
"bash": false,
"webfetch": true
"permission": {
"edit": "deny",
"bash": "ask",
"webfetch": "allow"
}
}
```
You can also use wildcards to control multiple tools at once. For example, to disable all tools from an MCP server:
You can also use wildcards to control multiple tools at once. For example, to require approval for all tools from an MCP server:
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"tools": {
"mymcp_*": false
"permission": {
"mymcp_*": "ask"
}
}
```
---
### Per agent
Override global tool settings for specific agents using the `tools` config in the agent definition.
```json title="opencode.json" {3-6,9-12}
{
"$schema": "https://opencode.ai/config.json",
"tools": {
"write": true,
"bash": true
},
"agent": {
"plan": {
"tools": {
"write": false,
"bash": false
}
}
}
}
```
For example, here the `plan` agent overrides the global config to disable `write` and `bash` tools.
You can also configure tools for agents in Markdown.
```markdown title="~/.config/opencode/agent/readonly.md"
---
description: Read-only analysis agent
mode: subagent
tools:
write: false
edit: false
bash: false
---
Analyze code without making any modifications.
```
[Learn more](/docs/agents#tools) about configuring tools per agent.
[Learn more](/docs/permissions) about configuring permissions.
---
@@ -101,8 +52,8 @@ Execute shell commands in your project environment.
```json title="opencode.json" {4}
{
"$schema": "https://opencode.ai/config.json",
"tools": {
"bash": true
"permission": {
"bash": "allow"
}
}
```
@@ -118,8 +69,8 @@ Modify existing files using exact string replacements.
```json title="opencode.json" {4}
{
"$schema": "https://opencode.ai/config.json",
"tools": {
"edit": true
"permission": {
"edit": "allow"
}
}
```
@@ -135,14 +86,18 @@ Create new files or overwrite existing ones.
```json title="opencode.json" {4}
{
"$schema": "https://opencode.ai/config.json",
"tools": {
"write": true
"permission": {
"edit": "allow"
}
}
```
Use this to allow the LLM to create new files. It will overwrite existing files if they already exist.
:::note
The `write` tool is controlled by the `edit` permission, which covers all file modifications (`edit`, `write`, `patch`, `multiedit`).
:::
---
### read
@@ -152,8 +107,8 @@ Read file contents from your codebase.
```json title="opencode.json" {4}
{
"$schema": "https://opencode.ai/config.json",
"tools": {
"read": true
"permission": {
"read": "allow"
}
}
```
@@ -169,8 +124,8 @@ Search file contents using regular expressions.
```json title="opencode.json" {4}
{
"$schema": "https://opencode.ai/config.json",
"tools": {
"grep": true
"permission": {
"grep": "allow"
}
}
```
@@ -186,8 +141,8 @@ Find files by pattern matching.
```json title="opencode.json" {4}
{
"$schema": "https://opencode.ai/config.json",
"tools": {
"glob": true
"permission": {
"glob": "allow"
}
}
```
@@ -203,8 +158,8 @@ List files and directories in a given path.
```json title="opencode.json" {4}
{
"$schema": "https://opencode.ai/config.json",
"tools": {
"list": true
"permission": {
"list": "allow"
}
}
```
@@ -224,8 +179,8 @@ This tool is only available when `OPENCODE_EXPERIMENTAL_LSP_TOOL=true` (or `OPEN
```json title="opencode.json" {4}
{
"$schema": "https://opencode.ai/config.json",
"tools": {
"lsp": true
"permission": {
"lsp": "allow"
}
}
```
@@ -243,14 +198,18 @@ Apply patches to files.
```json title="opencode.json" {4}
{
"$schema": "https://opencode.ai/config.json",
"tools": {
"patch": true
"permission": {
"edit": "allow"
}
}
```
This tool applies patch files to your codebase. Useful for applying diffs and patches from various sources.
:::note
The `patch` tool is controlled by the `edit` permission, which covers all file modifications (`edit`, `write`, `patch`, `multiedit`).
:::
---
### skill
@@ -260,14 +219,12 @@ Load a [skill](/docs/skills) (a `SKILL.md` file) and return its content in the c
```json title="opencode.json" {4}
{
"$schema": "https://opencode.ai/config.json",
"tools": {
"skill": true
"permission": {
"skill": "allow"
}
}
```
You can control approval prompts for loading skills via [permissions](/docs/permissions) using `permission.skill`.
---
### todowrite
@@ -277,8 +234,8 @@ Manage todo lists during coding sessions.
```json title="opencode.json" {4}
{
"$schema": "https://opencode.ai/config.json",
"tools": {
"todowrite": true
"permission": {
"todowrite": "allow"
}
}
```
@@ -286,7 +243,7 @@ Manage todo lists during coding sessions.
Creates and updates task lists to track progress during complex operations. The LLM uses this to organize multi-step tasks.
:::note
This tool is disabled for subagents by default, but you can enable it manually. [Learn more](/docs/agents/#tools)
This tool is disabled for subagents by default, but you can enable it manually. [Learn more](/docs/agents/#permissions)
:::
---
@@ -298,8 +255,8 @@ Read existing todo lists.
```json title="opencode.json" {4}
{
"$schema": "https://opencode.ai/config.json",
"tools": {
"todoread": true
"permission": {
"todoread": "allow"
}
}
```
@@ -307,7 +264,7 @@ Read existing todo lists.
Reads the current todo list state. Used by the LLM to track what tasks are pending or completed.
:::note
This tool is disabled for subagents by default, but you can enable it manually. [Learn more](/docs/agents/#tools)
This tool is disabled for subagents by default, but you can enable it manually. [Learn more](/docs/agents/#permissions)
:::
---
@@ -319,8 +276,8 @@ Fetch web content.
```json title="opencode.json" {4}
{
"$schema": "https://opencode.ai/config.json",
"tools": {
"webfetch": true
"permission": {
"webfetch": "allow"
}
}
```
@@ -329,6 +286,30 @@ Allows the LLM to fetch and read web pages. Useful for looking up documentation
---
### question
Ask the user questions during execution.
```json title="opencode.json" {4}
{
"$schema": "https://opencode.ai/config.json",
"permission": {
"question": "allow"
}
}
```
This tool allows the LLM to ask the user questions during a task. It's useful for:
- Gathering user preferences or requirements
- Clarifying ambiguous instructions
- Getting decisions on implementation choices
- Offering choices about what direction to take
Each question includes a header, the question text, and a list of options. Users can select from the provided options or type a custom answer. When there are multiple questions, users can navigate between them before submitting all answers.
---
## Custom tools
Custom tools let you define your own functions that the LLM can call. These are defined in your config file and can execute arbitrary code.