zeroclaw-labs/zeroclaw · error

{field_name} can only contain ASCII letters, numbers, and un

Error message

{field_name} can only contain ASCII letters, numbers, and underscores; got '{value}'

What it means

The final identifier rule: after the first character, every remaining character must be an ASCII letter, digit, or underscore. Anything else — spaces, dashes, dots, quotes, non-ASCII — is rejected because these identifiers are formatted directly into SQL statements and any other character would break the statement or enable injection through identifiers.

Source

Thrown at crates/zeroclaw-memory/src/postgres.rs:346

    })?
}

pub(super) fn validate_identifier(value: &str, field_name: &str) -> Result<()> {
    if value.is_empty() {
        anyhow::bail!("{field_name} must not be empty");
    }

    let mut chars = value.chars();
    let Some(first) = chars.next() else {
        anyhow::bail!("{field_name} must not be empty");
    };

    if !(first.is_ascii_alphabetic() || first == '_') {
        anyhow::bail!("{field_name} must start with an ASCII letter or underscore; got '{value}'");
    }

    if !chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_') {
        anyhow::bail!(
            "{field_name} can only contain ASCII letters, numbers, and underscores; got '{value}'"
        );
    }

    Ok(())
}

pub(super) fn quote_identifier(value: &str) -> String {
    format!("\"{value}\"")
}

fn recall_time_filter(since: bool, until: bool, first_placeholder: usize) -> String {
    match (since, until) {
        (true, true) => format!(
            " AND m.created_at >= ${first_placeholder}::TIMESTAMPTZ AND m.created_at <= ${}::TIMESTAMPTZ",
            first_placeholder + 1
        ),
        (true, false) => {

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Use snake_case for schema/table identifiers: replace dashes and spaces with underscores.
  2. Sanitize derived identifiers — keep [A-Za-z0-9_] and substitute everything else with an underscore.
  3. Trim whitespace when taking identifiers from user input or config.

Example fix

// before: alias "my-agent" -> "my-agent_memories" (rejected)
let table = format!("{agent}_memories", agent = alias);

// after: sanitize to the allowed charset
let table = format!("{}_memories", sanitize_identifier(&alias)); // "my_agent_memories"
Defensive patterns

Strategy: type-guard

Validate before calling

let table = sanitize_identifier(&format!("{alias}_memories"));
assert!(is_valid_pg_identifier(&table), "derived identifier failed validation");

Type guard

fn sanitize_identifier(raw: &str) -> String {
    let mut s: String = raw
        .chars()
        .map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' })
        .collect();
    if !s.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_') {
        s.insert(0, '_');
    }
    s
}

Prevention

When it happens

Trigger: Identifiers containing dashes (kebab-case aliases like my-agent), dots, spaces, quotes, or Unicode characters, passed to new or validated_schema_identifier for the postgres backend.

Common situations: Kebab-case agent or storage alias names used directly as schema/table names; names with trailing whitespace from copy-paste; internationalized names with accented characters.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/843e15d2c252a987. Report an issue: GitHub.