Hmbown/CodeWhale · error · anyhow::Error
invalid lane environment key {key:?}
Error message
invalid lane environment key {key:?} What it means
Thrown by write_lane_environment when an environment key fails the identifier check: the first character must be '_' or an ASCII letter, and the rest must be '_' or ASCII alphanumeric ([A-Za-z_][A-Za-z0-9_]*). Values are unrestricted — only key names are validated. This matches what a shell can legally export, since the pairs are written as a private lane environment file.
Source
Thrown at crates/lane/src/runtime.rs:234
match std::fs::remove_file(path) {
Ok(()) => Ok(()),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(err) => Err(err).with_context(|| format!("remove {}", path.display())),
}
}
fn valid_environment_key(key: &str) -> bool {
let mut chars = key.chars();
chars
.next()
.is_some_and(|ch| ch == '_' || ch.is_ascii_alphabetic())
&& chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
}
fn write_lane_environment(path: &Path, environment: &[(String, String)]) -> Result<()> {
for (key, _) in environment {
if !valid_environment_key(key) {
bail!("invalid lane environment key {key:?}");
}
}
let encoded = serde_json::to_vec(environment).context("serialize private lane environment")?;
if encoded.len() as u64 > MAX_ENVIRONMENT_BYTES {
bail!(
"private lane environment exceeds {} bytes",
MAX_ENVIRONMENT_BYTES
);
}
let tmp_path = lane_environment_tmp_path(path);
remove_file_if_present(path)?;
remove_file_if_present(&tmp_path)?;
let mut options = OpenOptions::new();
options.create_new(true).write(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;View on GitHub (pinned to 0c42157ee5)
Solutions
- Rename keys to identifier form: my-var -> MY_VAR, foo.bar -> FOO_BAR
- Filter/translate the environment at the boundary before building LaneStartSpec (map '-' and '.' to '_')
- Drop empty-string keys — they can never be valid
Example fix
// before
let spec = LaneStartSpec { environment: vec![("my-var".into(), "1".into())], .. };
// after
let spec = LaneStartSpec { environment: vec![("MY_VAR".into(), "1".into())], .. }; Defensive patterns
Strategy: type-guard
Validate before calling
fn valid_env_key(key: &str) -> bool {
let mut chars = key.chars();
chars.next().is_some_and(|c| c == '_' || c.is_ascii_alphabetic())
&& chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
}
let environment: Vec<(String, String)> = environment
.into_iter()
.filter(|(k, _)| valid_env_key(k))
.collect(); Type guard
fn is_valid_lane_env_key(key: &str) -> bool {
let mut chars = key.chars();
chars.next().is_some_and(|c| c == '_' || c.is_ascii_alphabetic())
&& chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
} Prevention
- Sanitize keys at the boundary: map '-' and '.' to '_', drop empties and leading digits
- Forward only an allowlist of variable names instead of the whole ambient environment
When it happens
Trigger: A LaneStartSpec.environment containing keys like "my-var" (hyphen), "123" (leading digit), "foo.bar" (dot), an empty string, or a non-ASCII/unicode key. Collecting the ambient environment of a tool that permits hyphenated names and passing it through unchanged triggers this on the first bad key.
Common situations: Forwarding a parent process env dump that includes keys other languages allow; machine-generated keys from config systems (dot-notation like server.port); placeholders like "-" or "" sneaking in from templating.
Related errors
- private lane environment exceeds {} bytes
- project workspace path cannot be empty
- terminal lane transition requires a terminal status
- unknown runtime backend `{other}` (use tmux|inline|vm|ci)
- serialized lane exit receipt exceeds size bound
AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20).
Data as JSON: /api/errors/9a88b229d65d839c.
Report an issue: GitHub.