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:
-
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.
-
Argument-eating — projects 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:
-
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.\"}
-
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.
Summary
The
projectsnamespace help describes a parsing rule: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 hadproj-001/proj-dupas legacy entries, plus a real active project.Repros
projects info / usage / schema / keyssilently swap targetsEach one also leaks the active project's
service_keyto stdout when the user thought they were inspecting a different project's keys.projects sqldrops the actual queryThe 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:
The error here is benign because
badly-typed-idisn'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:
Silent target swap — a user with
proj-001(a legacy keystore entry that pre-dates theprj_prefix convention) runsprojects 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.Argument-eating —
projects 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:
Refuse non-prefixed first positionals unless an explicit
--use-activeflag is set:At minimum, refuse extra positionals — if
projects sqlgot two arguments and the first isn't aprj_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-dupkeystore entriesThese showed up in
projects listoutput mixed with real projects: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.