{"record":{"id":"712306927b8ec5bc","repo":"Yeachan-Heo/oh-my-codex","slug":"mutation-not-allowed","errorCode":"mutation_not_allowed","errorMessage":"mutating Hermes bridge tools require allow_mutation: true","messagePattern":"mutating Hermes bridge tools require allow_mutation: true","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/mcp/hermes-bridge.ts","lineNumber":147,"sourceCode":"  error: string,\n): HermesBridgeResult<T> {\n  return { ok: false, code, error };\n}\n\nfunction normalizeString(value: unknown, name: string, options: { required?: boolean } = {}): string | undefined {\n  if (value == null) {\n    if (options.required) throw new Error(`${name} is required`);\n    return undefined;\n  }\n  if (typeof value !== \"string\") throw new Error(`${name} must be a string`);\n  const trimmed = value.trim();\n  if (!trimmed && options.required) throw new Error(`${name} must be non-empty`);\n  return trimmed || undefined;\n}\n\nfunction requireMutation(args: Record<string, unknown>): void {\n  if (args.allow_mutation !== true) {\n    throw new Error(\"mutating Hermes bridge tools require allow_mutation: true\");\n  }\n}\n\nfunction normalizePositiveInteger(value: unknown, fallback: number, max: number): number {\n  if (value == null) return fallback;\n  const parsed = typeof value === \"number\" ? value : Number.parseInt(String(value), 10);\n  if (!Number.isInteger(parsed) || parsed <= 0) return fallback;\n  return Math.min(parsed, max);\n}\n\nasync function readJsonFile<T>(path: string): Promise<T | null> {\n  try {\n    return safeJsonParse<T | null>(await readFile(path, \"utf-8\"), null);\n  } catch {\n    return null;\n  }\n}\n","sourceCodeStart":129,"sourceCodeEnd":165,"githubUrl":"https://github.com/Yeachan-Heo/oh-my-codex/blob/3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2/src/mcp/hermes-bridge.ts#L129-L165","documentation":"Hermes bridge tools that mutate state (submit answers, send prompts, start sessions, report status) refuse to run unless the caller explicitly passes allow_mutation: true. This is a safety interlock so MCP clients cannot accidentally trigger side effects.","triggerScenarios":"Calling hermesSubmitQuestionAnswer, hermesSendPrompt, hermesStartSession, or hermesReportStatus without allow_mutation: true in the args object.","commonSituations":"New client integration omitting the safety flag, or a cautious default in generated client code that strips unknown fields.","solutions":["Add allow_mutation: true to the tool arguments","Only do so for trusted, intentional mutations","Keep read-only tools (like hermesListQuestions) without the flag"],"exampleFix":"// before\nhermesSendPrompt({ session_id: \"s1\", prompt: \"go\" })\n// after\nhermesSendPrompt({ session_id: \"s1\", prompt: \"go\", allow_mutation: true })","handlingStrategy":"validation","validationCode":"if (toolMutates) args.allow_mutation = true; // set before calling","typeGuard":null,"tryCatchPattern":"catch (e) { if ((e as Error).message.includes('allow_mutation')) { if (userConfirmedIntent) { retry with allow_mutation: true } } }","preventionTips":["Treat allow_mutation as an explicit user consent step","Gate mutating calls behind UI confirmation","Keep read-only calls free of the flag"],"tags":["mcp","hermes-bridge","mutation-guard","safety"],"backgroundTag":"explicit-consent-flag-missing","analyzedSha":"3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2","analyzedAt":"2026-08-27T22:18:39.783Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}