Hmbown/CodeWhale · error · io::Error

Invalid session id '{session_id}'

Error message

Invalid session id '{session_id}'

What it means

ApprovalLog::validated_session_id guards log_path() before it joins <sessions_dir>/<session_id>/approval_receipts.jsonl. It trims the id, then rejects it if it is empty or contains any character outside ASCII letters, digits, '-', and '_'. This is a path-safety guard: spaces, dots, slashes, or '..' in a session id could escape the per-session directory, so malformed ids fail closed with InvalidInput before any filesystem access.

Source

Thrown at crates/tui/src/approval_log.rs:174

impl ApprovalReceiptStore {
    pub(crate) fn new(sessions_dir: PathBuf) -> Self {
        Self { sessions_dir }
    }

    #[cfg_attr(test, allow(dead_code))]
    pub(crate) fn default_location() -> io::Result<Self> {
        crate::session_manager::default_sessions_dir().map(Self::new)
    }

    fn validated_session_id(session_id: &str) -> io::Result<&str> {
        let trimmed = session_id.trim();
        if trimmed.is_empty()
            || !trimmed
                .chars()
                .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
        {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                format!("Invalid session id '{session_id}'"),
            ));
        }
        Ok(trimmed)
    }

    fn log_path(&self, session_id: &str) -> io::Result<PathBuf> {
        let session_id = Self::validated_session_id(session_id)?;
        Ok(self.sessions_dir.join(session_id).join(APPROVAL_LOG_FILE))
    }

    fn lock_path(&self, session_id: &str) -> io::Result<PathBuf> {
        let session_id = Self::validated_session_id(session_id)?;
        Ok(self.sessions_dir.join(session_id).join(APPROVAL_LOCK_FILE))
    }

    fn open_lock_file(&self, session_id: &str) -> io::Result<File> {

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Use the session id produced by the session manager (generated [A-Za-z0-9_-] form), not a display name
  2. Sanitize before calling: trim, then replace or reject characters outside [A-Za-z0-9_-]
  3. Validate the id at the input boundary (CLI arg, config, recorded session index) and fail with a user-facing message before touching approval logs

Example fix

// before
let receipts = approval_log.load("session 42");? // InvalidInput: Invalid session id 'session 42'

// after
let id = "session 42".chars().map(|c| if c.is_ascii_alphanumeric() || c == '-' || c == '_' { c } else { '_' }).collect::<String>();
let receipts = approval_log.load(&id);
Defensive patterns

Strategy: type-guard

Validate before calling

let id = session_id.trim();
assert!(!id.is_empty(), "session id required");

Type guard

fn is_valid_session_id(session_id: &str) -> bool {
    !session_id.is_empty()
        && session_id
            .chars()
            .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
}

Try / catch

match approval_log.load(sid) {
    Err(e) if e.kind() == io::ErrorKind::InvalidInput && e.to_string().contains("session id") => {
        // reject/sanitize the id at the caller; do not retry the same value
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling approval_log.load(session_id) / log_path(session_id) (directly or via replay/resume flows) with values like "session 2", "../other-session", "id:1234", "séance", or an empty/whitespace-only string. Leading/trailing whitespace is tolerated because the id is trimmed; any interior invalid character is not.

Common situations: Passing a user-typed session name containing spaces instead of the generated session id; passing a filesystem path or URL slug as an id; ids containing ':', '.', '/', or unicode copied from another tool.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/b27c4ada7c296bc0. Report an issue: GitHub.