Hmbown/CodeWhale · error · anyhow::Error

{} could not be parsed safely

Error message

{} could not be parsed safely

What it means

Codewhale parses the validated .env text with dotenvy::from_read_iter and collects every entry into a Vec. If dotenvy's parser rejects any line, the whole load aborts with '{path} could not be parsed safely'. The all-or-nothing collect is deliberate: a half-loaded credential set is worse than none.

Source

Thrown at crates/tui/src/lib.rs:2496

fn load_workspace_dotenv_credentials_from_path(path: &Path) -> Result<WorkspaceDotenvReport> {
    let contents = read_stable_workspace_dotenv(path)?;
    let text = std::str::from_utf8(&contents)
        .map_err(|_| anyhow!("{} is not valid UTF-8", path.display()))?;
    if dotenv_has_variable_expansion(text) {
        bail!(
            "{} uses variable expansion; workspace .env values must be literal to prevent ambient-secret substitution",
            path.display()
        );
    }

    let mut report = WorkspaceDotenvReport {
        path: path.to_path_buf(),
        ..WorkspaceDotenvReport::default()
    };
    let entries = dotenvy::from_read_iter(std::io::Cursor::new(contents))
        .collect::<std::result::Result<Vec<_>, _>>()
        .map_err(|_| anyhow!("{} could not be parsed safely", path.display()))?;
    for entry in entries {
        let (key, value) = entry;
        if !is_workspace_dotenv_credential_key(&key) {
            report.ignored.insert(key);
            continue;
        }
        if std::env::var_os(&key).is_some() {
            continue;
        }

        // SAFETY: this loader runs synchronously in `main` before the runtime
        // owner or Tokio workers are spawned. No concurrent environment reader
        // exists inside Codewhale, and later startup code treats this process
        // environment as immutable.
        unsafe { std::env::set_var(&key, value) };
        report.loaded.insert(key);
    }
    Ok(report)

View on GitHub (pinned to 8880682c63)

Solutions

  1. Check every line for balanced quotes and a single KEY=VALUE shape; wrap values containing spaces or # in double quotes on one line
  2. Keep PEM keys and long tokens on a single line, or store a file path instead of the literal secret
  3. Remove stray characters introduced by copy-paste (smart quotes, line-break artifacts)
  4. Lint the file by parsing each non-comment line as KEY=VALUE before rerunning

Example fix

# before
PRIVATE_KEY="-----BEGIN KEY-----
abc123
-----END KEY-----"

# after
PRIVATE_KEY_PATH=/etc/codewhale/key.pem
Defensive patterns

Strategy: validation

Validate before calling

for (i, line) in text.lines().enumerate() {
    let l = line.trim();
    if l.is_empty() || l.starts_with('#') {
        continue;
    }
    if !l.contains('=') {
        eprintln!("line {} has no '='", i + 1);
    }
    if let Some((_k, v)) = l.split_once('=') {
        if v.matches('"').count() % 2 == 1 {
            eprintln!("line {} has unbalanced quotes", i + 1);
        }
    }
}

Prevention

When it happens

Trigger: A line dotenvy cannot parse: an unterminated quote (KEY="value), invalid characters in a key position, malformed export syntax, or control characters that survived the UTF-8 check.

Common situations: Hand-edited .env with a missing closing quote; a multi-line PEM key pasted without single-line quoting; a copy-paste that split or merged lines; stray full-width or invisible characters.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/dc1db6d19fac6888. Report an issue: GitHub.