{"record":{"id":"1ffcbf52f1261b15","repo":"nanocoai/nanoclaw","slug":"settingsfile-hooks-must-be-a-json-object","errorCode":null,"errorMessage":"${settingsFile} hooks must be a JSON object","messagePattern":"(.+?) hooks must be a JSON object","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"container/agent-runner/src/providers/claude.ts","lineNumber":373,"sourceCode":"\nfunction claudeProjectsDir(): string {\n  return path.join(claudeConfigDir(), 'projects');\n}\n\nfunction claudeConfigDir(): string {\n  return process.env.CLAUDE_CONFIG_DIR || path.join(process.env.HOME || os.homedir(), '.claude');\n}\n\nfunction writeMemorySessionHook(hook: MemorySessionHookRegistration): void {\n  const configDir = claudeConfigDir();\n  const settingsFile = path.join(configDir, 'settings.json');\n  fs.mkdirSync(configDir, { recursive: true });\n\n  const parsed: unknown = fs.existsSync(settingsFile) ? JSON.parse(fs.readFileSync(settingsFile, 'utf-8')) : {};\n  if (!isRecord(parsed)) throw new Error(`${settingsFile} must contain a JSON object`);\n\n  const hooks = parsed.hooks === undefined ? {} : parsed.hooks;\n  if (!isRecord(hooks)) throw new Error(`${settingsFile} hooks must be a JSON object`);\n\n  const sessionStart = hooks.SessionStart === undefined ? [] : hooks.SessionStart;\n  if (!Array.isArray(sessionStart)) throw new Error(`${settingsFile} hooks.SessionStart must be an array`);\n\n  const memoryCommands = new Set([hook.command, ...hook.legacyCommands]);\n  const nextSessionStart = sessionStart\n    .map((entry) => removeMemoryCommands(entry, memoryCommands))\n    .filter((entry) => entry !== undefined);\n  nextSessionStart.push({\n    matcher: hook.sources.join('|'),\n    hooks: [{ type: 'command', command: hook.command, timeout: 10 }],\n  });\n\n  hooks.SessionStart = nextSessionStart;\n  parsed.hooks = hooks;\n  fs.writeFileSync(settingsFile, JSON.stringify(parsed, null, 2) + '\\n');\n}\n","sourceCodeStart":355,"sourceCodeEnd":391,"githubUrl":"https://github.com/nanocoai/nanoclaw/blob/294ef2aee85218b23ad30eda9dfe10e590b54a8c/container/agent-runner/src/providers/claude.ts#L355-L391","documentation":"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.","triggerScenarios":"author: {\"name\": {\"first\": \"Jane\"}} or author: {\"name\": 42}.","commonSituations":"Nested structured names; unquoted values; booleans.","solutions":["Flatten the value to a single string","Quote values that look numeric"],"exampleFix":"// before\n\"author\": { \"name\": { \"first\": \"Jane\" } }\n// after\n\"author\": { \"name\": \"Jane\" }","handlingStrategy":"validation","validationCode":"const bad = Object.entries(manifest.author ?? {}).filter(([, v]) => typeof v !== 'string');\nif (bad.length) { /* flatten values to strings before parse */ }","typeGuard":"function authorValuesOk(raw: Record<string, unknown>): boolean {\n  const a = raw.author;\n  return a === undefined || isPlainObject(a) && Object.values(a).every((v) => typeof v === 'string');\n}","tryCatchPattern":"try { parsePluginManifest(raw); } catch (e) { if (e instanceof Error && e.message.includes('author.') && e.message.includes('string')) { /* stringify the value */ } else throw e; }","preventionTips":["Keep author fields flat strings"],"tags":["json","manifest","type-validation"],"backgroundTag":"schema-validation-failed","analyzedSha":"294ef2aee85218b23ad30eda9dfe10e590b54a8c","analyzedAt":"2026-08-28T13:59:10.357Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}