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
- Flatten the value to a single string
- 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
- Keep author fields flat strings
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
- session_mode 'per-thread' requires honored thread ids, but t
- engage_mode 'pattern' requires --engage-pattern (use "." to
- ${flag} must be valid JSON
- bad request shape
- --stdin-json input must be one JSON object
AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28).
Data as JSON: /api/errors/1ffcbf52f1261b15.
Report an issue: GitHub.