nanocoai/nanoclaw · error

${settingsFile} hooks must be a JSON object

Error message

${settingsFile} hooks must be a JSON object

What it means

Every value inside the author object must be a string. A non-string value (number, object, array, boolean) for an allowed key throws with the key name interpolated.

Source

Thrown at container/agent-runner/src/providers/claude.ts:373

function claudeProjectsDir(): string {
  return path.join(claudeConfigDir(), 'projects');
}

function claudeConfigDir(): string {
  return process.env.CLAUDE_CONFIG_DIR || path.join(process.env.HOME || os.homedir(), '.claude');
}

function writeMemorySessionHook(hook: MemorySessionHookRegistration): void {
  const configDir = claudeConfigDir();
  const settingsFile = path.join(configDir, 'settings.json');
  fs.mkdirSync(configDir, { recursive: true });

  const parsed: unknown = fs.existsSync(settingsFile) ? JSON.parse(fs.readFileSync(settingsFile, 'utf-8')) : {};
  if (!isRecord(parsed)) throw new Error(`${settingsFile} must contain a JSON object`);

  const hooks = parsed.hooks === undefined ? {} : parsed.hooks;
  if (!isRecord(hooks)) throw new Error(`${settingsFile} hooks must be a JSON object`);

  const sessionStart = hooks.SessionStart === undefined ? [] : hooks.SessionStart;
  if (!Array.isArray(sessionStart)) throw new Error(`${settingsFile} hooks.SessionStart must be an array`);

  const memoryCommands = new Set([hook.command, ...hook.legacyCommands]);
  const nextSessionStart = sessionStart
    .map((entry) => removeMemoryCommands(entry, memoryCommands))
    .filter((entry) => entry !== undefined);
  nextSessionStart.push({
    matcher: hook.sources.join('|'),
    hooks: [{ type: 'command', command: hook.command, timeout: 10 }],
  });

  hooks.SessionStart = nextSessionStart;
  parsed.hooks = hooks;
  fs.writeFileSync(settingsFile, JSON.stringify(parsed, null, 2) + '\n');
}

View on GitHub (pinned to 294ef2aee8)

Solutions

  1. Flatten the value to a single string
  2. Quote values that look numeric

Example fix

// before
"author": { "name": { "first": "Jane" } }
// after
"author": { "name": "Jane" }
Defensive patterns

Strategy: validation

Validate before calling

const bad = Object.entries(manifest.author ?? {}).filter(([, v]) => typeof v !== 'string');
if (bad.length) { /* flatten values to strings before parse */ }

Type guard

function authorValuesOk(raw: Record<string, unknown>): boolean {
  const a = raw.author;
  return a === undefined || isPlainObject(a) && Object.values(a).every((v) => typeof v === 'string');
}

Try / catch

try { parsePluginManifest(raw); } catch (e) { if (e instanceof Error && e.message.includes('author.') && e.message.includes('string')) { /* stringify the value */ } else throw e; }

Prevention

When it happens

Trigger: author: {"name": {"first": "Jane"}} or author: {"name": 42}.

Common situations: Nested structured names; unquoted values; booleans.

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/1ffcbf52f1261b15. Report an issue: GitHub.