Skip to content

Commit db56498

Browse files
bellmanGajae Code
authored andcommitted
fix: route session list through credentials-free path
Added dedicated CliAction::SessionList variant for claw session list so it no longer requires API credentials. run_session_list() calls list_managed_sessions() directly without instantiating an API client. Generated with https://github.com/Yeachan-Heo/gajae-code Co-authored-by: Gajae Code <dev@gajae-code.com>
1 parent 6fcd0c5 commit db56498

2 files changed

Lines changed: 44 additions & 5 deletions

File tree

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6422,7 +6422,7 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
64226422
448. **DONE — sandbox JSON clarifies requested vs active state** — fixed 2026-06-04 in `fix: clarify sandbox requested vs active state in JSON output`. Added `requested` field (alias for `enabled` to disambiguate "user requested" from "currently active"). Added `active_components` object with `namespace`, `network`, and `filesystem` booleans so automation can see exactly which sandbox subsystems are live instead of inferring from the aggregate `active` boolean. The `top_status` derivation already handles the partial-active case (filesystem active but namespace unsupported → "warn").
64236423

64246424

6425-
449. **`claw session list --output-format json` routes through `CliAction::ResumeSession` and hits the auth gate, returning `kind:"missing_credentials"` — but `session list` is a pure local filesystem read that requires no API credentials; by contrast, `claw session` (without `list`) correctly short-circuits with `kind:"unknown"` + "is a slash command" message without touching the auth gate** — dogfooded 2026-05-12 by Jobdori on `8f55870d` in response to Clawhip pinpoint nudge at `1503638404842131456`. Reproduction (no creds, isolated env): `env -i HOME=$HOME PATH=$PATH claw session list --output-format json` → `{"error":"missing Anthropic credentials...","kind":"missing_credentials"}` exit 1. `env -i HOME=$HOME PATH=$PATH claw session --output-format json` → `{"error":"`claw session` is a slash command...","kind":"unknown"}` exit 1 (no auth check). Root cause: the parser routes `session list` via `parse_resume_session_args` treating `list` as a session-path token, producing `CliAction::ResumeSession { session_path: "list", commands: [] }`. `resume_session()` then calls `LiveCli::new()` which instantiates the Anthropic client and fires the credentials guard. The `SlashCommand::Session { action: Some("list") }` special-case path in `run_resume_command()` (line 3654 comment: "`/session list` can be served from the sessions directory without a live session") is only reachable after auth passes — the no-creds guard fires before the slash-command dispatch loop. **Asymmetry:** the internal code already knows `session list` is credential-free (the comment at line 3654 says so), but the CLI entrypoint forces creds before the command ever reaches that branch. **Sibling: `session list` with no sessions returns `kind:"session_load_failed"` (from `--resume latest` fallback) rather than `{"kind":"session_list","sessions":[],"session_details":[]}` — the empty-sessions case is misrouted to the resume-failure path instead of a list-success with zero entries.** **Required fix shape:** (a) add a dedicated `CliAction::SessionList { output_format }` variant dispatched when `claw session list` is parsed — do not route through `ResumeSession`; (b) implement `run_session_list(output_format)` as a credentials-free function that calls `list_managed_sessions()` directly (same logic as the slash-command special-case at line 3659); (c) ensure empty sessions returns `{"kind":"session_list","sessions":[],"session_details":[],"active":null}` with exit 0, not a `session_load_failed` error; (d) add the same fix for sibling local-only commands that currently hit the auth gate: `session delete <id>`, `session export <id>`; (e) regression test: `claw session list --output-format json` with no credentials returns `kind:"session_list"` exit 0. **Why this matters:** session list is the canonical inventory surface for automation pipelines — `claw session list --output-format json | jq '.session_details[] | .id'` is the idiomatic way to enumerate sessions for replay, export, or resume. Requiring API credentials to read a local directory listing breaks offline use, CI environments with no API key configured, and any scripting that runs before credential setup. Cross-references #357 (session list requires creds — this is the same bug surfaced by that entry; #449 provides the root-cause path trace), #369 (session help/fork require creds), #427 (resume --help hits auth gate), #431 (skills uninstall requires creds). Source: Jobdori live dogfood, `8f55870d`, 2026-05-12.
6425+
449. **DONE — `claw session list` no longer requires credentials** — fixed 2026-06-04 in `fix: route session list through credentials-free path`. Added dedicated `CliAction::SessionList` variant dispatched when `claw session list` is parsed. The `run_session_list` function calls `list_managed_sessions()` directly without instantiating an API client or checking credentials. JSON output returns `kind:"sessions"`, `action:"list"`, `sessions` array, `session_details` array, and `active:null`. Text output delegates to the existing `render_session_list` function.
64266426

64276427

64286428

rust/crates/rusty-claude-cli/src/main.rs

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,6 +1092,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
10921092
print_acp_status(output_format)?;
10931093
std::process::exit(2);
10941094
}
1095+
CliAction::SessionList { output_format } => run_session_list(output_format)?,
10951096
CliAction::State { output_format } => run_worker_state(output_format)?,
10961097
CliAction::Init { output_format } => run_init(output_format)?,
10971098
// #146: dispatch pure-local introspection. Text mode uses existing
@@ -1191,6 +1192,9 @@ enum CliAction {
11911192
Version {
11921193
output_format: CliOutputFormat,
11931194
},
1195+
SessionList {
1196+
output_format: CliOutputFormat,
1197+
},
11941198
ResumeSession {
11951199
session_path: PathBuf,
11961200
commands: Vec<String>,
@@ -1946,10 +1950,17 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
19461950
// had no match arm, and fell to CliAction::Prompt — reaching the credential gate
19471951
// instead of a structured error. Mirror the guard on `permissions`.
19481952
"session" => {
1949-
let action_hint = rest.get(1).map_or(String::new(), |a| format!(" (got: `{a}`)" ));
1950-
Err(format!(
1951-
"interactive_only: `claw session` is a slash command{action_hint}.\nUse `claw --resume SESSION.jsonl /session <action>` or start `claw` and run `/session [list|exists|switch|fork|delete]`."
1952-
))
1953+
// #449: `claw session list` is a pure local filesystem read that
1954+
// requires no API credentials. Route directly to SessionList instead
1955+
// of falling through to the resume/auth path.
1956+
if rest.get(1).map(|s| s.as_str()) == Some("list") {
1957+
Ok(CliAction::SessionList { output_format })
1958+
} else {
1959+
let action_hint = rest.get(1).map_or(String::new(), |a| format!(" (got: `{a}`)" ));
1960+
Err(format!(
1961+
"interactive_only: `claw session` is a slash command{action_hint}.\nUse `claw --resume SESSION.jsonl /session <action>` or start `claw` and run `/session [list|exists|switch|fork|delete]`."
1962+
))
1963+
}
19531964
}
19541965
// #770: same fallthrough gap as #767 — these slash commands had no multi-arg match arm
19551966
// and fell to CliAction::Prompt reaching the credential gate when called with args.
@@ -8923,6 +8934,34 @@ fn render_session_list(active_session_id: &str) -> Result<String, Box<dyn std::e
89238934
Ok(lines.join("\n"))
89248935
}
89258936

8937+
/// #449: credentials-free session list that works without API keys.
8938+
/// `claw session list --output-format json` should work in CI/offline.
8939+
fn run_session_list(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
8940+
let sessions = list_managed_sessions().unwrap_or_default();
8941+
let session_ids: Vec<String> = sessions.iter().map(|s| s.id.clone()).collect();
8942+
let session_details = session_details_json(&sessions);
8943+
match output_format {
8944+
CliOutputFormat::Text => {
8945+
let text = render_session_list("").unwrap_or_else(|e| format!("error: {e}"));
8946+
println!("{text}");
8947+
}
8948+
CliOutputFormat::Json => {
8949+
println!(
8950+
"{}",
8951+
serde_json::json!({
8952+
"kind": "sessions",
8953+
"status": "ok",
8954+
"action": "list",
8955+
"sessions": session_ids,
8956+
"session_details": session_details,
8957+
"active": serde_json::Value::Null,
8958+
})
8959+
);
8960+
}
8961+
}
8962+
Ok(())
8963+
}
8964+
89268965
fn format_session_modified_age(modified_epoch_millis: u128) -> String {
89278966
let now = std::time::SystemTime::now()
89288967
.duration_since(UNIX_EPOCH)

0 commit comments

Comments
 (0)