{"record":{"id":"4e9370558cbca553","repo":"nanocoai/nanoclaw","slug":"settingsfile-hooks-sessionstart-must-be-an-arra","errorCode":null,"errorMessage":"${settingsFile} hooks.SessionStart must be an array","messagePattern":"(.+?) hooks\\.SessionStart must be an array","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"container/agent-runner/src/providers/claude.ts","lineNumber":376,"sourceCode":"}\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\nfunction removeMemoryCommands(value: unknown, commands: ReadonlySet<string>): unknown {\n  if (!isRecord(value) || !Array.isArray(value.hooks)) return value;\n  const hooks = value.hooks.filter((hook) => {","sourceCodeStart":358,"sourceCodeEnd":394,"githubUrl":"https://github.com/nanocoai/nanoclaw/blob/294ef2aee85218b23ad30eda9dfe10e590b54a8c/container/agent-runner/src/providers/claude.ts#L358-L394","documentation":"lintSecrets scans MCP server config values and throws when a value matches a real-credential pattern (SECRET_VALUE_RE) instead of the literal placeholder. An auth-scheme prefix like 'Bearer ' is stripped before matching, so 'Bearer sk-...' still trips it. Templates must ship the placeholder; operators inject real secrets after stamping.","triggerScenarios":"A server entry value (env/header/url param) containing something that looks like an API key or token (e.g. 'sk-...', 'ghp_...', 'Bearer sk-...') rather than the PLACEHOLDER_VALUE literal.","commonSituations":"Developers pasting a working local config with real keys into a template they intend to commit; testing with real credentials and forgetting to scrub before publishing.","solutions":["Replace the real credential with the literal placeholder value (see PLACEHOLDER_VALUE in mcp.ts)","Move the real secret to your local/operator config, outside the template","If the match is a false positive, rename or restructure the value so it doesn't match SECRET_VALUE_RE"],"exampleFix":"// before\n{ \"headers\": { \"Authorization\": \"Bearer sk-live-abc123\" } }\n// after\n{ \"headers\": { \"Authorization\": \"{{SECRET}}\" } }  // use the exact PLACEHOLDER_VALUE literal","handlingStrategy":"validation","validationCode":"for (const [key, value] of Object.entries(configValues)) {\n  const bare = String(value).replace(/^(Bearer|Token|Basic)\\s+/i, '');\n  if (value !== PLACEHOLDER_VALUE && SECRET_VALUE_RE.test(bare)) {\n    throw new Error(`refusing to ship real credential in ${key}`);\n  }\n}","typeGuard":"function isSafeTemplateValue(value: string): boolean {\n  return value === PLACEHOLDER_VALUE || !SECRET_VALUE_RE.test(value.replace(/^(Bearer|Token|Basic)\\s+/i, ''));\n}","tryCatchPattern":"try { readServerEntry(...); } catch (e) { if (e instanceof Error && e.message.includes('looks like a real credential')) { /* scrub value to PLACEHOLDER_VALUE, fail the publish */ } else throw e; }","preventionTips":["Never paste working credentials into template files; develop with the placeholder from day one","Run secret scanners (gitleaks/trufflehog) over template repos in CI","Keep real secrets in operator-side config injected after stamping"],"tags":["security","secrets","mcp","templates"],"backgroundTag":"secret-leak-prevention","analyzedSha":"294ef2aee85218b23ad30eda9dfe10e590b54a8c","analyzedAt":"2026-08-28T13:59:10.357Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}