Skip to content

Bash permission 'ask' hangs forever in headless/server mode #14473

@jpcarranza94

Description

@jpcarranza94

Description

When running OpenCode in headless/server mode (opencode serve), bash commands that trigger an external_directory permission check with the default "ask" action hang forever. The tool call enters status=running and never completes, permanently freezing the session.

Root Cause

The bash tool (packages/opencode/src/tool/bash.ts, lines ~116-164) performs two separate permission checks for file-manipulating commands (cat, rm, cp, mv, mkdir, touch, chmod, chown):

  1. An external_directory permission check — fires first when realpath resolves a file path outside the project directory
  2. A bash permission check — fires second for the command pattern itself

When a user configures bash deny rules:

"bash": {
  "*": "deny",
  "some_command *": "allow"
}

This only creates rules for the bash permission. The external_directory permission defaults to "ask" (packages/opencode/src/agent/agent.ts, lines ~55-73).

In packages/opencode/src/permission/next.ts (lines ~127-156), "ask" creates a Promise that waits for user input. In headless/server mode, there is nobody to respond, so the Promise never resolves and the session is permanently frozen.

Why Symlinks Trigger This

Instance.containsPath() compares against Instance.directory (set from process.cwd(), which does NOT resolve symlinks), but the bash tool resolves file arguments via realpath (which DOES resolve all symlinks).

Common cases where this mismatch occurs:

  • macOS: /tmp/private/tmp, /var/private/var
  • Symlinked project directories: any project using symlinks for subdirectories
  • Docker volumes: bind mounts that resolve differently inside the container

If the project is a git repo, Instance.worktree (from git rev-parse --show-toplevel, which resolves symlinks) provides a fallback check. But non-git projects set worktree = "/" which is skipped.

Reproduction Steps

  1. Start OpenCode in server mode:
mkdir /tmp/opencode-test
cat > /tmp/opencode-test/opencode.json << 'CONF'
{
  "permission": {
    "read": "allow",
    "bash": {
      "*": "deny",
      "echo *": "allow"
    }
  }
}
CONF
mkdir -p /tmp/opencode-test/subdir
echo "test content" > /tmp/opencode-test/subdir/file.txt
cd /tmp/opencode-test && opencode serve --port 4097
  1. Create a session and send a message that forces a cat command:
SESSION=$(curl -s -X POST http://localhost:4097/session \
  -H "Content-Type: application/json" \
  -H "x-opencode-directory: /tmp/opencode-test" \
  -d '{}' | jq -r '.id')

curl -s -X POST "http://localhost:4097/session/$SESSION/message" \
  -H "Content-Type: application/json" \
  -H "x-opencode-directory: /tmp/opencode-test" \
  -d '{"parts":[{"type":"text","text":"Use the bash tool to run: cat subdir/file.txt\nYou MUST use bash, not read."}]}'
  1. Check session state — the bash tool call is stuck at status=running forever:
curl -s "http://localhost:4097/session/$SESSION/message" \
  -H "x-opencode-directory: /tmp/opencode-test" | jq '.[].parts[] | select(.type=="tool")'

Key Observations

Command File Exists? realpath outside project? Result
cat subdir/file.txt yes yes (macOS /tmp symlink) HANGS
cat subdir/NONEXISTENT.md no N/A (realpath fails) error (denied)
ls -la /tmp N/A N/A (ls not in realpath list) error (denied)

Suggested Fixes

  1. "ask" must not hang in headless mode: Fall back to "deny" or time out when no UI is available to respond
  2. Resolve Instance.directory via realpath: So path comparisons are consistent with how realpath resolves file arguments
  3. Document external_directory: Users configuring "bash": {"*": "deny"} have no way of knowing a hidden secondary check can override their deny rules

Workaround

Adding "external_directory": "allow" to the permission config bypasses the hanging check:

"permission": {
  "external_directory": "allow",
  "bash": { "*": "deny", "specific_command *": "allow" }
}

Environment

  • OpenCode version: 1.2.10
  • Mode: headless server (opencode serve)
  • OS: macOS (Darwin) and Linux (Docker)

Metadata

Metadata

Assignees

Labels

coreAnything pertaining to core functionality of the application (opencode server stuff)

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions