Skip to content

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

Closed

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)

Activity

  1. github-actions commented on Feb 20, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    If your issue is distinct from these, please clarify the differences.

  2. added
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    on Feb 20, 2026
  3. jpcarranza94 commented on Feb 20, 2026

    @jpcarranza94
    ContributorAuthor

    Thanks for flagging these.

    #7876 describes the same root cause — external_directory permission defaulting to "ask" and hanging in headless mode. This issue adds a specific trigger scenario: the realpath vs process.cwd() symlink mismatch in the bash tool causes files that are inside the project to appear "outside" it, triggering the external_directory check unexpectedly. This happens on macOS (/tmp → /private/tmp) and with any symlinked project directories.

    #13715 is related but broader (nested subagent sessions). The underlying problem is the same: "ask" has no fallback in contexts without a UI responder.

    Happy to close this as a duplicate of #7876 if the symlink mismatch detail is captured there. The two issues are:

    1. "ask" hanging in headless mode (same as Permission check for tool-output directory never matches, causes hang in headless mode #7876)
    2. Instance.directory not being realpath-resolved, causing false external_directory triggers (new detail)
  4. github-actions commented on Apr 22, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions