Skip to content

session.list ignores location[directory] and returns sessions from every project #57635

Open

Description

@rupeshtr78

Description

GET /api/session silently ignores the location[directory] query parameter and returns the newest 50 sessions across every project. Every other location-scoped endpoint on the same server uses that shape (location.get, command.list, session.command, session.skill), so it is the natural parameter for a client to send. session.list instead expects a flat directory field, and the unknown location[...] key is dropped without an error, so the request silently degrades to an unscoped list.

Because DELETE /api/session/:id is not location scoped either, "list the sessions in this directory and delete them" deletes sessions from other projects.

Steps to reproduce

Two projects, one shared data directory (isolated so nothing else interferes):

  1. Start a server with cwd /tmp/rp/a and create a session:
    XDG_DATA_HOME=/tmp/rpdata OPENCODE_PASSWORD=p opencode serve --port 4133
    curl -u opencode:p -X POST localhost:4133/api/session -H 'content-type: application/json' -d '{"title":"A"}'
    
  2. Start a server with cwd /tmp/rp/b on the same XDG_DATA_HOME and create a session the same way.
  3. List on either server:
    GET /api/session?location[directory]=/tmp/rp/a   -> "B", "A"   (both projects)
    GET /api/session?directory=/tmp/rp/a             -> "A"        (flat field filters correctly)
    GET /api/session                                 -> "B", "A"
    
  4. Delete with the same scoping:
    DELETE /api/session/<session-in-B>?location[directory]=/tmp/rp/a  -> 204
    
    The session in project B is gone.

Expected

  • With a location scoping parameter, the list is limited to that directory, or the server rejects the unknown parameter instead of ignoring it.
  • A delete issued for a location does not silently affect a session that belongs to another project.

Actual

  • location[directory] is ignored by session.list; the response is the global newest-50 list across all projects.
  • session.remove has no location middleware, so it deletes any session by id regardless of the caller's location.

Impact

This is not theoretical. A cleanup script ran GET /api/session?location[directory]=<scratch dir> and then DELETE /api/session/:id for each returned id, expecting to remove its own scratch sessions. It deleted 49 sessions spanning 7 projects in one pass, including the session that was running the script. All 49 were recovered from the SQLite freelist, but that only worked because the pages had not been reused yet.

Pointers

  • packages/protocol/src/groups/session.ts: SessionsQuery inherits Session.ListFilter.fields (directory, project, subpath as flat fields) and has no location parameter, unlike command.list and location.get.
  • session.remove is declared without .middleware(sessionLocationMiddleware), while session.command and session.skill have it.

Version / OS

opencode 2.1.1, Ubuntu 24.04, Linux.

Activity

  1. opencode-agent commented on Oct 21, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed write-up, and sorry that cleanup script took out sessions from other projects. Glad you got them back.

    As the API is defined right now, the server is doing what it was built to do:

    • GET /api/session documents flat directory / project / subpath filters (SessionsQuery in packages/protocol/src/groups/session.ts). location[directory] isn't one of its parameters, so it's dropped like any other unknown query key, and the list comes back unfiltered. As you found, ?directory= filters correctly.
    • DELETE /api/session/:id deletes a session by its ID, which is unique across all projects. Adding sessionLocationMiddleware wouldn't scope it to your location: that middleware works out the location from the session being acted on, not from the request.

    The naming really is inconsistent with location.get, command.list and the other location-scoped endpoints, and it's easy to get wrong in a way that loses data. So I'm recording this as a request to harden the API. That could mean having session.list accept location[directory] or reject unknown query parameters instead of ignoring them, and optionally letting deletes check a stated location. Maintainers will consider it.

    Until then, ?directory=<path> on session.list scopes the list correctly. You can also check each returned session's location before deleting it.

    Related: #56197 (a closed PR that changed how the session list filters work).

  2. Dante-dan commented on Oct 21, 2026

    @Dante-dan
    Contributor

    A bounded compatibility proposal: accept the explicitly supplied location[directory] as an alias for the existing list directory filter, while keeping unfiltered listing and ID-only deletion available. Two details in the current implementation need explicit decisions before coding:

    1. Pagination must preserve the asserted scope. Session.list takes its filters from the cursor when one is supplied, ignoring filters alongside it. Simply mapping the alias to directory would therefore still allow a request carrying location[directory]=A and an unscoped/B cursor to return other locations. I suggest mapping the alias into the existing directory filter on the first page and returning InvalidRequestError when the alias and flat directory disagree, or for a cursor whose stored directory differs from the explicitly asserted directory. Cursor-only pagination would retain its current behavior. The existing directory-filtered pagination test is a useful compatibility baseline.
    2. A delete guard is a precondition on the target, not implicit process scoping. Keep omission of location as today's ID-only operation. If an explicit location is accepted on DELETE, compare it with the target session before interruption, transport closure, or deletion; a mismatch must have no side effects. Session.remove recursively removes children, so checking only the target must not be documented as guaranteeing that every descendant has that directory. I would preserve the existing cascade contract in this slice; subtree isolation would need its own agreed semantics.

    Unlike the closed #56197, this would not add restrictions to existing directory/project/subpath combinations; the new consistency check applies only when the explicit location alias is used.

    This keeps the existing generated client's flat list parameters and ID-only remove compatible, rather than changing omitted-location calls to mean the server's cwd or header. The protocol/OpenAPI and regenerated client should expose the alias and delete precondition consistently. Acceptance checks would cover alias/flat equivalence, conflicting selectors, cursor-scope mismatch, unchanged cursor-only pagination, and a mismatched delete leaving the target and children untouched.

    This is source-level analysis, not a reproduction of the reported recovery incident. Would this scope be suitable for design approval?

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions