Skip to content

web: session in nested project folder opens at git repo root, nested .opencode/skills not discovered #51191

Open

Description

@emad-elsaid

Summary

Starting a session in a project whose folder is a subdirectory of a git repository creates the session at the repository root instead of the picked folder. As a result, everything discovery-scoped to the picked folder — most notably .opencode/skills/ — is not loaded for the session.

Environment

  • opencode version: 2.0.12 (server; web UI served via opencode serve --port 4000)
  • OS: Arch Linux x86_64, kernel 7.2.6-arch2-1
  • Terminal: n/a (web UI in a browser)
  • Shell: /usr/bin/bash
  • Install/channel: latest channel, native binary at /usr/bin/opencode
  • Active plugins: none

Reproduction

  1. Have a git repo at ~/Documents and a subdirectory ~/Documents/knowledgebase containing .opencode/skills/<id>/SKILL.md files (valid frontmatter with name + description).
  2. In the web UI, open ~/Documents/knowledgebase — it appears in the project list as its own project, separate from Documents.
  3. Start a new session in that project.
  4. Inspect the session: GET /api/session/{sessionID} shows location.directory = ~/Documents (the git repo root), and the session's available skills do not include the skills from knowledgebase/.opencode/skills/.

Expected Behavior

The session should be rooted at the opened folder (~/Documents/knowledgebase). The server API already models this correctly — creating a session with an explicit nested directory keeps the directory and records the relationship to the parent project via subpath:

POST /api/session {"location":{"directory":"~/Documents/knowledgebase"}}
→ {"data":{"projectID":"24386f…","subpath":"knowledgebase",
           "location":{"directory":"~/Documents/knowledgebase"}}}

Skill discovery then works for that location:

GET /api/skill?location[directory]=~/Documents/knowledgebase
→ opencode, report, bug-finder, nested-skill-1 … nested-skill-6   (9 skills)

GET /api/skill?location[directory]=~/Documents
→ opencode, report, bug-finder                         (nested skills missing)

Actual Behavior

The session created by the web UI is registered at the git repo root, so skill discovery — which scans .opencode/skills from the session directory upward and never into child directories — cannot see knowledgebase/.opencode/skills/. None of the nested skills are advertised to the model. The project path shown for the session is also the repo root rather than the opened folder.

Additional Context

Activity

  1. Franklittleboy commented on Sep 28, 2026

    @Franklittleboy

    I’m experiencing the same issue on OpenCode v2.0.16 in the Web UI. I open two subfolders of the same Git repository as separate projects, but new sessions started from either project are created at the repository root instead. Existing sessions remain correctly associated with their respective subfolders.

  2. aspectmoonday-create commented on Oct 26, 2026

    @aspectmoonday-create

    Confirmation + additional scope for this bug, verified on v2.1.2 (Linux x64, web UI served by opencode serve, sources at ed74ec8).

    Still reproducible in 2.1.2

    Repo /home/agent/work (git root), subfolder /home/agent/work/Test added via Add project in the web UI, then New session (the pencil button on the project card) + one message:

    sqlite3 ~/.local/share/opencode/opencode.db \
      "select id, directory, path, project_id from session_v2 order by time_created desc limit 1;"
    ses_e88b2486dffePfhdANs9gd76tS|/home/agent/work||global
    

    Expected directory=/home/agent/work/Test, path=Test. The server is fine: POST /api/session with {"location":{"directory":"/home/agent/work/Test"}} stores directory=/home/agent/work/Test, path=Test.

    The composer is not the only place the path is lost

    #51805 and #57358 fix resolveSessionDirectory() in packages/app/src/new-session/composer-adapter.ts, which is necessary but not sufficient. Three entry points never reach that resolver because they open the draft with project.worktree directly:

    1. packages/app/src/home/projects/view.tsx — project card pencil (data-action="home-project-new-session") and the card menu item command.session.new both call onOpenProjectNewSession(server, props.project.worktree).
    2. packages/app/src/home/model.ts — openNewSession() calls openProjectNewSession(conn, project.worktree); home.selection only stores the project root, so the folder the user picked at Add project time is dropped in project.add (setSelection({ ..., directory: project.worktree })).
    3. packages/app/src/shell/titlebar/titlebar.tsx — the home branch of the new-tab handler calls tabs.newDraft({ ..., directory: project.worktree }).

    Evidence that the composer change alone does not help: with resolveSessionDirectory() returning the draft's own directory (the #57358 shape) in a locally built v2.1.2, a session started from the project card still produced the row above (directory=/home/agent/work), because the draft itself was opened at the repo root.

    What worked locally

    Keeping the chosen folder end to end:

    • persist the picked folder per project (home.directories keyed by server + worktree, written in project.add and when a session in a nested folder is opened);
    • openProjectNewSession() resolves the target through the project (selected folder if it is inside the project, otherwise the root) instead of taking project.worktree from the callers;
    • resolveSessionDirectory() returns the draft's location directory for worktree === "main" (worktree === "create" and mcp.prepare still use the canonical project directory).

    Result on the same machine, browser-driven, isolated XDG_* data dir, repo/subfolder as in the repro:

    entry point before after
    Add project .../proj/sub → project card pencil .../proj .../proj/sub (path=sub)
    Add project .../proj/sub → Home “New session” .../proj .../proj/sub (path=sub)
    project root as the chosen folder .../proj .../proj (unchanged)
    folder outside git that folder that folder (unchanged)

    packages/app typecheck and test:unit (889) pass.

    Also worth noting for the PWA-served web UI: after upgrading the binary, an already loaded tab keeps the old bundle (registerType: "prompt", skipWaiting: false, clientsClaim: false), so a plain reload can still report the old behaviour. Closing all tabs or a private window shows the new build.

  3. wally720 commented on Oct 27, 2026

    @wally720

    I can reproduce this in OpenCode Web on Windows. When I select a project folder that is a subdirectory of a Git repository and create a new Local session, the session's location.directory is set to the repository root instead of the selected subfolder.

    Expected: the new session should use the selected subfolder. Creating a session through POST /api/session with an explicit location.directory for that subfolder worked as a workaround, which suggests the issue is in the Web UI's new-session destination handling. I don't have the exact OpenCode version from my original reproduction.

  4. opencodejr commented on Oct 28, 2026

    @opencodejr

    Thanks for the detailed report. I reproduced this on v2.0.12, and it still happens on the latest dev (9adc42b).

    The script below sets up a git repo with a nested knowledgebase/.opencode/skills/nested-skill, starts opencode serve and makes the same server calls the web app makes when you start a new session:

    • For the nested folder, GET /api/location returns project.canonical = the repo root.
    • The new-session code in the web app (packages/app/src/new-session/composer-adapter.ts) uses that project.canonical as the session directory when the main worktree is selected. The session is therefore created at the repo root without a subpath, and the opened folder is dropped.
    • Skills listed for the repo root don't include nested-skill; for the nested folder they do.

    Your workaround still applies: moving the session to the nested folder (POST /api/session/{id}/move) makes the nested skills load.

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Reproduces anomalyco/opencode#51191: the web app's new-session flow creates the
    # session at location.get(...).project.canonical (the git repo root) instead of
    # the nested folder that was opened, so nested .opencode/skills are not loaded.
    # Usage: ./repro.sh            (uses `opencode` on PATH)
    #        OC="bun ... packages/cli/src/index.ts" ./repro.sh   (run from source)
    set -euo pipefail
    OC=${OC:-opencode}
    PORT=${PORT:-4517}
    ROOT=$(mktemp -d /tmp/opencode/repro51191.XXXX)
    export HOME=$ROOT/home XDG_CONFIG_HOME=$ROOT/home/.config XDG_DATA_HOME=$ROOT/home/.local/share XDG_STATE_HOME=$ROOT/home/.local/state XDG_CACHE_HOME=$ROOT/home/.cache
    mkdir -p "$HOME"
    REPO=$ROOT/Documents; NESTED=$REPO/knowledgebase
    mkdir -p "$NESTED/.opencode/skills/nested-skill"
    cat > "$NESTED/.opencode/skills/nested-skill/SKILL.md" <<'EOF'
    ---
    name: nested-skill
    description: Skill that lives in the nested project folder
    ---
    Hello
    EOF
    git -C "$REPO" init -q && git -C "$REPO" -c user.email=a@b -c user.name=a commit -q --allow-empty -m init
    
    cd "$NESTED"
    export OPENCODE_PASSWORD=repro
    C="curl -s -u opencode:repro"
    $OC serve --port $PORT >"$ROOT/server.log" 2>&1 &
    SRV=$!; trap 'kill $SRV 2>/dev/null' EXIT
    for i in $(seq 60); do curl -sf -u opencode:repro "http://127.0.0.1:$PORT/api/location?location%5Bdirectory%5D=$NESTED" >/dev/null && break; sleep 1; done
    B="http://127.0.0.1:$PORT/api"
    enc() { python3 -c 'import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1],safe=""))' "$1"; }
    
    echo "== GET /api/location for the opened nested folder (what the web app reads)"
    LOC=$($C "$B/location?location%5Bdirectory%5D=$(enc "$NESTED")"); echo "$LOC"
    CANON=$(echo "$LOC" | python3 -c 'import json,sys;d=json.load(sys.stdin);d=d.get("data",d);print(d["project"]["canonical"])')
    echo "project.canonical = $CANON   (opened folder = $NESTED)"
    
    echo "== Create session the way packages/app new-session/composer-adapter.ts does (worktree=main -> project.canonical)"
    S=$($C -X POST "$B/session" -H 'content-type: application/json' -d "{\"location\":{\"directory\":\"$CANON\"}}"); echo "$S"
    
    echo "== Skills visible at the session's directory vs the opened folder"
    echo "at $CANON:";  $C "$B/skill?location%5Bdirectory%5D=$(enc "$CANON")"  | python3 -c "import json,sys;d=json.load(sys.stdin);d=d.get(\"data\",d) if isinstance(d,dict) else d;print(sorted(x.get(\"name\") or x.get(\"id\") for x in d))"
    echo "at $NESTED:"; $C "$B/skill?location%5Bdirectory%5D=$(enc "$NESTED")" | python3 -c "import json,sys;d=json.load(sys.stdin);d=d.get(\"data\",d) if isinstance(d,dict) else d;print(sorted(x.get(\"name\") or x.get(\"id\") for x in d))"

    Run it: bash repro.sh

  5. pilzprinz commented on Oct 28, 2026

    @pilzprinz

    Also affects OpenCode Desktop 2.2.0 on Windows — and the impact is wider than undiscovered .opencode/skills. With the session rooted at the repo root, agent tool calls polluted an unrelated parent folder, and all subprojects collapsed into a single global project.

    Setup

    • D:\Documents\!Programs — a normal folder of unrelated project directories (Krita Plugins\Pigment Mixing, Books\Spectre of black rose, …) that ended up with a .git directory (created 2026-04-28 by an unrelated tool: zero commits, no remote).
    • Opened the subfolder D:\Documents\!Programs\Krita Plugins\Pigment Mixing as a project in Desktop 2.2.0 (service cli\2.2.0-x64), Windows 11, x64.

    Result

    1. Session rooted at the repo root → cross-project file pollution. New sessions get directory = D:\Documents\!Programs instead of the picked folder (same mechanism as this report; packages/app/src/new-session/composer-adapter.ts replaces the picked directory with project.canonical, present in the v2.2.0 tag). Concrete damage:

    • The agent compiled a Krita plugin test harness with an MSVC invocation using relative output paths (/Fo"test_pigment.obj" …). The resulting COFF objects — test_pigment.obj, test_pigment_cold.obj, bench_pigment.obj, KisPigmentMix.obj (2026-10-23 12:36 local) — landed in D:\Documents\!Programs, not in the Krita plugin folder. bench_pigment.obj embeds the literal path D:\Documents\!Programs\bench_pigment.obj, proving the compiler's CWD was the repo root.
    • First boot location was the Pigment Mixing folder (2026-10-22T20:03Z); the session "Реалистичное смешивание цветов в Krita" created at 20:06Z already had directory = repo root. Files from two unrelated projects ended up interleaved in one folder — from the user's perspective "my projects got mixed together".

    2. Repo with no commits/remote collapses every subproject into the global project. Because Project.resolve can't derive a project ID (no HEAD commit, no remote → ID.global), all sessions inside that repo fold into one undifferentiated project/session list:

    • session_v2 table: 119 sessions, all with directory = D:/Documents/!Programs and path (subpath) = ''. Not a single row points at the actual subfolders (Krita plugin, book, etc.).
    • Desktop open tabs (state store tabs.info) show the same: Krita-plugin sessions, book-editing sessions, multi-agent translation sessions — all stamped with the repo root as directory.
    • Sessions for projects outside that repo (e.g. a Telegram bot in D:\Games\...) have correct per-folder directories, confirming the pattern.

    3. Second entry point. The home screen hits the same code path: openProjectNewSession(conn, project.worktree) in packages/app/src/home/model.ts uses the worktree (repo root) as the session location, so opening a project card reproduces the bug without going through the new-session composer.

    Impact beyond skill discovery

    • Agent tool calls with relative output paths write files into the repo root; cleanup requires manually triaging which project each file belongs to.
    • Session history/search becomes unusable: sessions of completely unrelated projects are grouped under one project row.

    Workaround that worked for me

    • Re-root an existing session at the real subfolder via the session move endpoint (POST /session/{id}/move with the target directory).
    • Or start sessions from the CLI inside the subfolder so the directory is correct from the start.
    • Files already written to the root had to be moved back manually.

    (The desktop variant was reported in #57583 and closed as a duplicate of this issue; the bug is still present in 2.2.0, so adding the Windows/desktop evidence here. Happy to provide DB dumps or more traces if useful.)

  6. Mac-Moneysac commented on Oct 29, 2026

    @Mac-Moneysac

    I have the same problem with version 2.2.1

  7. hyxieshi commented on Nov 3, 2026

    @hyxieshi

    Same here on Windows with OpenCode Desktop 2.2.5. I open a frontend subfolder inside a Git repo, but the session ends up using the repo root as its working directory instead of the folder I selected. There’s no worktree involved, just a regular subfolder. Looks like the same issue — is #57358 the fix to follow?

  8. ortunom commented on Nov 11, 2026

    @ortunom

    Same issue with OpenCode 2.2.7 on macOS (27.0.1) in the web UI.

    Setup

    • ~/.git exists. The home repository has no commits or configured remotes.
    • ~/Documents has no .git directory of its own, so Git considers it part of the home repository.

    Observed behavior

    I select ~/Documents in the web UI. The sidebar and the session show my home folder as the location instead of ~/Documents.

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