Skip to content

Can't delete a workspace whose folder was removed outside OpenCode (DELETE /api/worktree → 400) #57987

Open

Description

@lambert-zhh

Description

OpenCode keeps a workspace (git worktree) after the worktree is removed outside OpenCode. Deleting it in the desktop app fails, so the stale workspace can't be removed from the UI.

Environment

  • OpenCode Desktop 2.1.2 (arm64), macOS 27.0.1

Steps to reproduce (what happened in my case)

  1. Create a git worktree outside OpenCode, e.g. git -C <repo> worktree add ../.worktrees/<name> -b <name>.
  2. Work in it with OpenCode. The service registers it on its own: it runs git worktree list --porcelain from that folder and saves it as a workspace of the repo's project (worktree table, strategy = git).
  3. Remove the worktree outside OpenCode. The folder is gone and git worktree list no longer shows it.
  4. In the desktop app, delete the workspace.

Expected

Deleting a workspace whose folder no longer exists removes the saved record. Better: the app drops it automatically once git no longer lists it.

Actual

The app shows an error and the workspace stays. Service log, three attempts:

message="Sent HTTP response" http.method=DELETE http.url=/api/worktree http.status=400

GET /api/worktree?projectID=<id> keeps returning the deleted directory.

Workaround

Re-syncing the project with git removes it:

opencode api worktree.refresh -d '{"projectID":"<project id>"}'

Suggested fix

  • In worktree.remove: if the directory is missing or git no longer lists the worktree, drop the saved record instead of returning 400 (or offer "Remove from list").
  • Or run the worktree.refresh reconciliation when the app lists workspaces.

Activity

  1. opencode-agent commented on Oct 22, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I reproduced this on 2.1.2 through the server API, and it still happens on the latest dev (248bfbe).

    Steps:

    1. Create a git worktree outside OpenCode with git worktree add ../wt -b wt, and use it as a location so it's saved as a git worktree of the project.
    2. Remove it with git worktree remove ../wt.
    3. DELETE /api/worktree for that directory returns 400 Worktree directory unavailable: <dir>, with force: false and with force: true.
    4. GET /api/worktree keeps listing the directory until POST /api/worktree/refresh runs, which matches your workaround.

    The script below runs all of this against an isolated opencode serve.

    It looks like the remove path checks that the folder exists before it looks up and deletes the saved record, so a folder deleted outside OpenCode can't be removed this way.

    Related: #54827 (reload fails after a managed worktree folder is removed outside OpenCode) and #51172 (on Windows, a partial removal leaves a stale record).

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Repro for #57987: DELETE /api/worktree returns 400 for a saved git worktree whose
    # folder was removed outside OpenCode; the record stays until worktree.refresh.
    # Usage: ./repro.sh            (uses `opencode` from PATH)
    #        OC_DEV=/path/to/checkout ./repro.sh   (runs the CLI from source)
    set -u
    W=$(mktemp -d /tmp/opencode/r57987.XXXX)
    export HOME=$W/home XDG_DATA_HOME=$W/data XDG_CONFIG_HOME=$W/config XDG_STATE_HOME=$W/state XDG_CACHE_HOME=$W/cache
    mkdir -p $HOME
    export OPENCODE_PASSWORD=repro
    PORT=${PORT:-4517}
    if [ -n "${OC_DEV:-}" ]; then
      OC=(bun --conditions=browser --preload=$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js $OC_DEV/packages/cli/src/index.ts)
    else
      OC=(opencode)
    fi
    echo "version: $("${OC[@]}" --version)"
    
    REPO=$W/repo
    mkdir -p $REPO && cd $REPO
    git init -q -b main && git -c user.email=a@b -c user.name=a commit -q --allow-empty -m init
    git worktree add -q ../wt -b wt
    WT=$W/wt
    
    "${OC[@]}" serve --hostname 127.0.0.1 --port $PORT > $W/serve.log 2>&1 &
    SP=$!
    trap 'kill $SP 2>/dev/null' EXIT
    for i in $(seq 1 60); do curl -u opencode:repro -sf http://127.0.0.1:$PORT/api/project >/dev/null 2>&1 && break; sleep 1; done
    S=http://127.0.0.1:$PORT
    
    # Use the worktree as a location so the project is registered, then sync worktrees.
    curl -u opencode:repro -s -o /dev/null -w "boot location: %{http_code}\n" "$S/api/agent?location%5Bdirectory%5D=$WT"
    PID=$(curl -u opencode:repro -s $S/api/project | python3 -c 'import sys,json;d=json.load(sys.stdin);d=d if isinstance(d,list) else d.get("data",d);print(d[0]["id"])')
    echo "projectID: $PID"
    curl -u opencode:repro -s -o /dev/null -w "refresh: %{http_code}\n" -X POST $S/api/worktree/refresh -H 'content-type: application/json' -d "{\"projectID\":\"$PID\"}"
    echo "list before: $(curl -u opencode:repro -s "$S/api/worktree?projectID=$PID")"
    
    # Remove the worktree outside OpenCode.
    git -C $REPO worktree remove ../wt
    echo "git worktree list: $(git -C $REPO worktree list | wc -l) entr(y/ies); wt exists: $([ -e $WT ] && echo yes || echo no)"
    
    echo "DELETE /api/worktree:"
    curl -u opencode:repro -s -w "\n  -> %{http_code}\n" -X DELETE $S/api/worktree -H 'content-type: application/json' -d "{\"projectID\":\"$PID\",\"directory\":\"$WT\",\"force\":false}"
    echo "DELETE /api/worktree (force):"
    curl -u opencode:repro -s -w "\n  -> %{http_code}\n" -X DELETE $S/api/worktree -H 'content-type: application/json' -d "{\"projectID\":\"$PID\",\"directory\":\"$WT\",\"force\":true}"
    echo "list after delete: $(curl -u opencode:repro -s "$S/api/worktree?projectID=$PID")"
    curl -u opencode:repro -s -o /dev/null -w "refresh: %{http_code}\n" -X POST $S/api/worktree/refresh -H 'content-type: application/json' -d "{\"projectID\":\"$PID\"}"
    echo "list after refresh: $(curl -u opencode:repro -s "$S/api/worktree?projectID=$PID")"

    Run it: bash repro.sh

  2. wthsama commented on Oct 29, 2026

    @wthsama

    Same class of problem as reported here, but for project records rather than worktree rows.

    Environment: OpenCode Desktop 2.2.1, Windows 11, bundled opencode-cli 2.2.1-x64 (server runs as opencode-cli.exe serve --service).

    What happened

    1. Created three git worktrees next to the repo (git worktree add ../wt/<name> main) and ran sessions in them.
    2. Removed them outside OpenCode (git worktree remove --force ... + git worktree prune).
    3. The three directories still show up in the desktop workspaces panel, each with "0 sessions".

    They are projects, not worktrees: GET /api/project returns them with canonical = the deleted directory, no vcs field, and GET /api/worktree?projectID=<id> returns [] for each.

    What I tried against the running server

    • POST /api/worktree/refresh {projectID: <main repo>} → 204, no effect.
    • POST /api/worktree/refresh {projectID: <stale>} → 400 Worktree directory unavailable: D:\...\wt\admin.
    • DELETE /api/worktree {projectID, directory, force: true} → 400 Invalid worktree directory: D:\...\wt\admin, even after recreating the directory and registering it as a real git worktree again.
    • PATCH /api/project/{id} {"archived": true} → 200, but the field is dropped (additionalProperties: false; only canonical, name, icon, commands are accepted) and the row is still returned by GET /api/project?archived=false.
    • DELETE /api/project/{id} → 404, no such route.
    • Clearing the desktop client state (Local Storage, IndexedDB, drafts.sqlite) → no effect, so the records live server-side.

    Expected: a project whose canonical directory no longer exists should be dropped automatically (same reconciliation worktree.refresh is supposed to do for worktrees), or at least be removable. Accepting archived in project.update would also be enough to hide it.

    Is there a supported way to remove such a project in 2.2.1?

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions