nanocoai/nanoclaw · error · Error

${flag} must be a JSON object with string values

Error message

${flag} must be a JSON object with string values

What it means

parseStringRecord (used for MCP `headers` and `env`) rejects a value that is present but not a plain object — arrays, null, strings, or numbers all fail. The first check (container-config.ts:203) fires before per-entry inspection.

Source

Thrown at src/container-config.ts:203

  for (const key of Object.keys(env)) {
    if (!ENV_KEY_RE.test(key)) {
      throw new Error(`env key ${JSON.stringify(key)} must be a valid environment variable name`);
    }
  }
  const cwd = parseCwd(input.cwd);
  return {
    command,
    args,
    env,
    ...(cwd === undefined ? {} : { cwd }),
    ...(instructions === undefined ? {} : { instructions }),
  };
}

function parseStringRecord(value: unknown, flag: string): Record<string, string> | undefined {
  if (value === undefined) return undefined;
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
    throw new Error(`${flag} must be a JSON object with string values`);
  }
  const record: Record<string, string> = {};
  for (const [key, entry] of Object.entries(value)) {
    if (typeof entry !== 'string') throw new Error(`${flag} must be a JSON object with string values`);
    record[key] = entry;
  }
  return record;
}

/** Accept only the spec's fixed cwd shapes, lexically contained (no ".." segments). */
function parseCwd(value: unknown): string | undefined {
  if (value === undefined) return undefined;
  if (typeof value !== 'string' || !CWD_FORM_RE.test(value)) {
    throw new Error('cwd must be ./path, ${PLUGIN_ROOT}[/path], or ${PLUGIN_DATA}[/path]');
  }
  // rest === '' is the bare form (`${PLUGIN_DATA}`, `./`); empty segments in a
  // non-empty rest are rejected for symmetry with the command validator.
  const rest = value.startsWith('./') ? value.slice(2) : value.replace(CWD_FORM_RE, '');

View on GitHub (pinned to 294ef2aee8)

Solutions

  1. Provide a JSON object: {"headers":{"Authorization":"Bearer x"}}
  2. Check shell quoting so the flag value parses as JSON, not a string
  3. Remove the field if unused (undefined is accepted)

Example fix

// before
{"headers":"X-Api-Key: abc"}
// after
{"headers":{"X-Api-Key":"abc"}}
Defensive patterns

Strategy: type-guard

Validate before calling

if (v !== undefined && (typeof v !== 'object' || v === null || Array.isArray(v))) throw new UserError('must be a JSON object');

Type guard

function isStringRecord(v: unknown): v is Record<string, string> { return typeof v === 'object' && v !== null && !Array.isArray(v) && Object.values(v).every(x => typeof x === 'string'); }

Try / catch

catch (err) { if (err.message.includes('JSON object with string values')) rePromptAsObject(); else throw err; }

Prevention

When it happens

Trigger: headers: "Authorization: Bearer x" (string), headers: ["a"], env: null passed via the config flag whose name is interpolated into the message (`flag`).

Common situations: Passing YAML-ish or HTTP-text header blocks instead of JSON objects; shell quoting that turns the object into a string; null from optional-field templating.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28). Data as JSON: /api/errors/14c3cd3bface782f. Report an issue: GitHub.