Skip to content

Session paths split by casing on case-insensitive macOS volumes #44014

Open

Description

@AidenGeunGeun

Description

On the default case-insensitive macOS filesystem, the same directory can be opened with different component casing. realPath preserves the caller spelling, so OpenCode stores separate Location keys and exact directory filters can hide existing sessions.

Plugins

None

OpenCode version

V2 at 2c8e2a2b2

Steps to reproduce

  1. Create /tmp/Academic/Project on a case-insensitive APFS volume.
  2. Create a session using /tmp/academic/project.
  3. List sessions using /tmp/Academic/Project.
  4. The created session is missing because the stored path uses the caller casing.

Screenshot and/or share link

N/A

Operating System

macOS on case-insensitive APFS

Terminal

Any

Activity

  1. iBobik commented on Sep 16, 2026

    @iBobik

    Related end-user workaround for macOS-hosted Docker devcontainers using virtiofs:

    Until paths are canonicalized centrally, deny known wrong-case paths for every filesystem-capable tool, including glob:

    {
      "permission": {
        "read": { "*": "allow", "app/App.vue": "deny" },
        "edit": { "*": "allow", "app/App.vue": "deny" },
        "glob": { "*": "allow", "*App.vue*": "deny" },
        "bash": { "*": "allow", "*app/App.vue*": "deny" }
      }
    }

    Adapt the path to the canonical casing in your repository. Also instruct agents to use casing exactly as returned by Git.

    If the duplicate entry already exists:

    1. Stop OpenCode.
    2. Normalize the filename from the macOS host.
    3. Remove the affected OpenCode snapshot.
    4. Fully restart Docker Desktop.

    Do not run rm against the wrong-case alias inside the container. On a case-insensitive host, that may delete the real file.

  2. iBobik commented on Sep 17, 2026

    @iBobik

    It was happening again, this seems solved it:

      "snapshot": false
    
  3. opencodejr commented on Oct 28, 2026

    @opencodejr

    Thanks for the report, I was able to reproduce this.

    I didn't have macOS, so I used a case-insensitive FAT volume on Linux as a stand-in for case-insensitive APFS:

    1. Created /mnt/ci44014/Academic/Project.
    2. Created a session through POST /api/session with location.directory set to /mnt/ci44014/academic/project. It is stored with that exact casing.
    3. GET /api/session?directory=/mnt/ci44014/academic/project returned the session, but GET /api/session?directory=/mnt/ci44014/Academic/Project returned {"data":[]}.

    This happens on 2.2.0 and still happens on the latest dev (a882cf98a). The script below reproduces it.

    Related issues, most closely related first:

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Reproduces #44014 on Linux using a case-insensitive vfat loop mount
    # (stand-in for case-insensitive APFS). Needs root (mount) + dosfstools + curl.
    # OC: command to run opencode (default: opencode from PATH).
    set -euo pipefail
    OC=${OC:-opencode}
    PORT=${PORT:-4599}
    export OPENCODE_SERVER_PASSWORD=repro
    WORK=$(mktemp -d /tmp/oc44014.XXXX)
    export HOME=$WORK/home XDG_DATA_HOME=$WORK/data XDG_CONFIG_HOME=$WORK/config XDG_STATE_HOME=$WORK/state XDG_CACHE_HOME=$WORK/cache
    mkdir -p "$HOME"
    
    MNT=/mnt/ci44014
    if ! mountpoint -q $MNT; then
      dd if=/dev/zero of=$WORK/fat.img bs=1M count=64 status=none
      mkfs.vfat $WORK/fat.img >/dev/null
      mkdir -p $MNT && mount -o loop $WORK/fat.img $MNT
    fi
    mkdir -p $MNT/Academic/Project
    echo "same inode for both spellings: $(stat -c %i $MNT/Academic/Project) $(stat -c %i $MNT/academic/project)"
    
    $OC --version
    cd $WORK
    setsid $OC serve --hostname 127.0.0.1 --port $PORT >$WORK/serve.log 2>&1 &
    SRV=$!
    trap 'kill -- -$SRV 2>/dev/null || true' EXIT
    for i in $(seq 1 60); do curl -sf -u opencode:repro "http://127.0.0.1:$PORT/api/session" >/dev/null 2>&1 && break; sleep 1; done
    URL=http://127.0.0.1:$PORT; AUTH=(-u opencode:repro)
    
    echo "== create session with $MNT/academic/project"
    curl -s "${AUTH[@]}" -X POST "$URL/api/session" -H 'content-type: application/json' \
      -d "{\"location\":{\"directory\":\"$MNT/academic/project\"},\"title\":\"lower\"}" | head -c 400
    echo
    for d in "$MNT/academic/project" "$MNT/Academic/Project"; do
      echo "== list sessions ?directory=$d"
      curl -s "${AUTH[@]}" -G "$URL/api/session" --data-urlencode "directory=$d" | head -c 200
      echo
    done

    Run it: bash repro.sh

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions