Skip to content

projects sql/info/usage/schema/keys silently route to the active project when the id positional doesn't start with 'prj_' — extra positionals after that are dropped #184

@MajorTal

Description

@MajorTal

Summary

The projects namespace help describes a parsing rule:

<id> is the project_id shown in 'run402 projects list' (prefix: 'prj_')
Most commands that take <id> default to the active project when omitted (set it with 'run402 projects use '). Project IDs start with 'prj_'; any first positional that doesn't is treated as the next argument instead.

In practice this means: pass a typo'd or shortened project id and the command silently runs against the active project — no warning, no error, no diff in the response shape that the caller can branch on. Worse, for commands with multiple positionals (sql, rest, apply-expose), the real later argument is dropped on the floor.

Tested on run402@1.53.0. Fresh keystore had proj-001 / proj-dup as legacy entries, plus a real active project.

Repros

projects info / usage / schema / keys silently swap targets

$ run402 projects info \"proj-001\"
{ \"project_id\": \"prj_1777563179844_1095\", ... anon_key ... service_key ... }   # active project, not proj-001!

$ run402 projects schema \"proj-001\"
{ \"schema\": \"p1095\", \"tables\": [{ \"name\": \"activity_log\" }, ...] }       # active project's schema

$ run402 projects usage \"proj-001\"
{ \"project_id\": \"prj_1777563179844_1095\", \"api_calls\": 212, ... }             # active project's usage

$ run402 projects keys \"proj-001\"
{ \"project_id\": \"prj_1777563179844_1095\", \"anon_key\": \"...\", \"service_key\": \"...\" }

Each one also leaks the active project's service_key to stdout when the user thought they were inspecting a different project's keys.

projects sql drops the actual query

$ run402 projects sql \"proj-001\" \"SELECT 1\"
{\"status\":\"error\",\"http\":400,\"error\":\"SQL error: syntax error at or near \\\"proj\\\"\",\"message\":\"SQL error: syntax error at or near \\\"proj\\\"\",\"code\":\"VALIDATION_FAILED\",...}

The CLI ran SQL(\"proj-001\") against the active project and silently dropped \"SELECT 1\". The user's actual query never executed. (And the error mentions \"proj\" which is what tipped me off.)

The dangerous version of this:

$ run402 projects sql badly-typed-id \"DELETE FROM users\"
# DELETE FROM users is dropped on the floor; \"badly-typed-id\" is run as SQL on the active project, errors out.

The error here is benign because badly-typed-id isn't valid SQL. But if the typo'd ID happens to also be a valid SQL fragment, the consequences are worse.

Why this matters

Two failure modes:

  1. Silent target swap — a user with proj-001 (a legacy keystore entry that pre-dates the prj_ prefix convention) runs projects info proj-001, gets back keys for a different project. They paste those keys into a script. The script now talks to the wrong DB.

  2. Argument-eatingprojects sql <bad_id> \"<query>\" drops the query. Combined with the silent target swap, this is a classic shoot-yourself-in-the-foot pattern.

The behavior is documented but the docs frame it as a feature ("any first positional that doesn't [start with prj_] is treated as the next argument instead") rather than a footgun. Most CLI tools that accept optional positional ids require an explicit sentinel (- or empty string) to skip them, or refuse the call.

Suggested fix

Two options:

  1. Refuse non-prefixed first positionals unless an explicit --use-active flag is set:

    $ run402 projects info \"proj-001\"
    {\"status\":\"error\",\"code\":\"BAD_PROJECT_ID\",\"message\":\"Argument 'proj-001' is not a project id (must start with 'prj_'). Did you mean to omit it and use the active project? Pass --active to confirm.\"}
    
  2. At minimum, refuse extra positionals — if projects sql got two arguments and the first isn't a prj_ id, error out instead of silently treating the first as query and dropping the second.

Option 1 is the safer default; option 2 closes the immediately-dangerous path with one line of code per affected subcommand.

Bonus: the legacy proj-001 / proj-dup keystore entries

These showed up in projects list output mixed with real projects:

$ run402 projects list | head
[{ \"project_id\": \"proj-001\", \"active\": false }, { \"project_id\": \"proj-dup\", \"active\": false }, { \"project_id\": \"prj_...\" }, ...]

Out of 31 entries, 2 had no prj_ prefix. The CLI doesn't flag them as legacy or stale, so the user can't tell which entries are real-on-server and which are orphans.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions