nanocoai/nanoclaw · critical
${settingsFile} hooks.SessionStart must be an array
Error message
${settingsFile} hooks.SessionStart must be an array What it means
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.
Source
Thrown at container/agent-runner/src/providers/claude.ts:376
}
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');
}
function removeMemoryCommands(value: unknown, commands: ReadonlySet<string>): unknown {
if (!isRecord(value) || !Array.isArray(value.hooks)) return value;
const hooks = value.hooks.filter((hook) => {View on GitHub (pinned to 294ef2aee8)
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
Example fix
// before
{ "headers": { "Authorization": "Bearer sk-live-abc123" } }
// after
{ "headers": { "Authorization": "{{SECRET}}" } } // use the exact PLACEHOLDER_VALUE literal Defensive patterns
Strategy: validation
Validate before calling
for (const [key, value] of Object.entries(configValues)) {
const bare = String(value).replace(/^(Bearer|Token|Basic)\s+/i, '');
if (value !== PLACEHOLDER_VALUE && SECRET_VALUE_RE.test(bare)) {
throw new Error(`refusing to ship real credential in ${key}`);
}
} Type guard
function isSafeTemplateValue(value: string): boolean {
return value === PLACEHOLDER_VALUE || !SECRET_VALUE_RE.test(value.replace(/^(Bearer|Token|Basic)\s+/i, ''));
} Try / catch
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; } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- engage_mode '${w.engage_mode}' can never engage on channel '
- chat-sdk bridge instance ${JSON.stringify(config.instance)}
- no verb "${verbArg}" on ${res.plural} — run `ncl ${res.plura
- --${column.name.replace(/_/g, '-')} must be a number
- Template ref must be relative to the templates directory: "$
AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28).
Data as JSON: /api/errors/4e9370558cbca553.
Report an issue: GitHub.