Repository navigation
Description
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):
- An
external_directorypermission check — fires first whenrealpathresolves a file path outside the project directory - A
bashpermission 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
- 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- Create a session and send a message that forces a
catcommand:
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."}]}'- Check session state — the bash tool call is stuck at
status=runningforever:
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
"ask"must not hang in headless mode: Fall back to"deny"or time out when no UI is available to respond- Resolve
Instance.directoryviarealpath: So path comparisons are consistent with howrealpathresolves file arguments - 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
github-actions commentedon Feb 20, 2026
This issue might be a duplicate of existing issues. Please check:
askaction hangs indefinitely in headless/automated mode when there is no UI to respondIf your issue is distinct from these, please clarify the differences.
jpcarranza94 commentedon Feb 20, 2026
Thanks for flagging these.
#7876 describes the same root cause —
external_directorypermission defaulting to"ask"and hanging in headless mode. This issue adds a specific trigger scenario: therealpathvsprocess.cwd()symlink mismatch in the bash tool causes files that are inside the project to appear "outside" it, triggering theexternal_directorycheck 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:
"ask"hanging in headless mode (same as Permission check for tool-output directory never matches, causes hang in headless mode #7876)Instance.directorynot beingrealpath-resolved, causing falseexternal_directorytriggers (new detail)feat(server): idle + bash-stuck detectors in sendPrompt watcher
feat(server): classify errors + auto-repair opencode.json on ensureSe…
feat(server): idle + bash-stuck detectors in sendPrompt watcher
feat(server): classify errors + auto-repair opencode.json on ensureSe…
github-actions commentedon Apr 22, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.
Headless permission base layer lands in the opencode-sdk spawn: compo…