actualbudget/actual · error · Error

Invalid config file: key "${key}" must be a string, got ${ty

Error message

Invalid config file: key "${key}" must be a string, got ${typeof v}

What it means

After confirming a config key is known, validateConfigFileContent checks its value type. String-typed keys (serverUrl, password, sessionToken, syncId, dataDir, encryptionPassword) must hold strings; a number, boolean, or null instead produces this error naming the key and the actual type found.

Source

Thrown at packages/cli/src/config.ts:86

function validateConfigFileContent(value: unknown): ConfigFileContent {
  if (!isRecord(value)) {
    throw new Error(
      'Invalid config file: expected an object with keys: ' +
        configFileKeys.join(', '),
    );
  }
  for (const key of Object.keys(value)) {
    if (!configFileKeys.includes(key)) {
      throw new Error(`Invalid config file: unknown key "${key}"`);
    }
    const v = value[key];
    if (v === undefined) continue;
    if (
      (stringKeys as readonly string[]).includes(key) &&
      typeof v !== 'string'
    ) {
      throw new Error(
        `Invalid config file: key "${key}" must be a string, got ${typeof v}`,
      );
    }
    if (
      (numberKeys as readonly string[]).includes(key) &&
      (typeof v !== 'number' || !Number.isInteger(v) || v < 0)
    ) {
      throw new Error(
        `Invalid config file: key "${key}" must be a non-negative integer`,
      );
    }
    if (
      (booleanKeys as readonly string[]).includes(key) &&
      typeof v !== 'boolean'
    ) {
      throw new Error(
        `Invalid config file: key "${key}" must be a boolean, got ${typeof v}`,
      );

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Quote the value so it is a string: "password": "12345"
  2. Remove the key entirely if the value is unknown (undefined-valued keys are skipped, absent keys are fine)
  3. For YAML, quote values that look numeric: syncId: "01234567"

Example fix

// before (actual.config.json)
{ "password": 12345, "dataDir": "./data" }

// after
{ "password": "12345", "dataDir": "./data" }
Defensive patterns

Strategy: type-guard

Validate before calling

const STRING_KEYS = ['serverUrl','password','sessionToken','syncId','dataDir','encryptionPassword'];
for (const k of STRING_KEYS) {
  if (cfg[k] !== undefined && typeof cfg[k] !== 'string') {
    throw new Error(`Config key "${k}" must be a string (quote numeric-looking values)`);
  }
}

Type guard

function stringsAreStrings(v: Record<string, unknown>, keys: readonly string[]): boolean {
  return keys.every(k => v[k] === undefined || typeof v[k] === 'string');
}

Try / catch

try {
  await run(['actual', 'accounts']);
} catch (e) {
  if (String(e.message).includes('must be a string')) {
    console.error('Quote the named key\'s value in your config file so it parses as a string');
  } else throw e;
}

Prevention

When it happens

Trigger: Writing `"cacheTtl": ...` confusion aside, the typical case is e.g. `"password": 12345` (unquoted number) or `"dataDir": null` explicitly present; YAML configs where a value like `no: yes` coerces oddly or an unquoted port-like string becomes a number; JS config exporting `syncId: undefined` is skipped, but `syncId: 42` throws.

Common situations: Unquoted numeric passwords or API tokens in JSON/YAML; templating tools injecting unquoted values; hand-editing YAML where implicit typing turns "0123" into 123.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/02b04f93547cc1d3. Report an issue: GitHub.