{"record":{"id":"be0169063427dc1e","repo":"thedotmack/claude-mem","slug":"missing-cwd-in-posttooluse-hook-input-for-session","errorCode":null,"errorMessage":"Missing cwd in PostToolUse hook input for session ${sessionId}, tool ${toolName}","messagePattern":"Missing cwd in PostToolUse hook input for session (.+?), tool (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"src/cli/handlers/observation.ts","lineNumber":55,"sourceCode":"  logger.debug('HOOK', 'Observation sent successfully via worker', { toolName: input.toolName });\n  return { continue: true, suppressOutput: true };\n}\n\nexport const observationHandler: EventHandler = {\n  async execute(input: NormalizedHookInput): Promise<HookResult> {\n    const { sessionId, cwd, toolName, toolInput, toolResponse } = input;\n    const platformSource = normalizePlatformSource(input.platform);\n\n    if (!toolName) {\n      return { continue: true, suppressOutput: true, exitCode: HOOK_EXIT_CODES.SUCCESS };\n    }\n\n    const toolStr = logger.formatTool(toolName, toolInput);\n\n    logger.dataIn('HOOK', `PostToolUse: ${toolStr}`, {});\n\n    if (!cwd) {\n      throw new Error(`Missing cwd in PostToolUse hook input for session ${sessionId}, tool ${toolName}`);\n    }\n\n    if (!shouldTrackProject(cwd)) {\n      logger.debug('HOOK', 'Project excluded from tracking, skipping observation', { cwd, toolName });\n      return { continue: true, suppressOutput: true };\n    }\n\n    const runtime = resolveRuntimeContext();\n    // Phase 1a (cmem-sdk rename): `runtime.runtime` is the canonical `'server'`\n    // value. `runtime-selector.selectRuntime()` continues to accept the legacy\n    // `'server-beta'` literal in settings.json and normalizes it to `'server'`.\n    if (runtime.runtime === 'server') {\n      const event: ServerRecordEventRequest = {\n        projectId: runtime.projectId,\n        contentSessionId: sessionId,\n        platformSource,\n        sourceType: 'hook',\n        eventType: 'tool_use',","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/e2d1df569a8f04075d40e92461128ece7cf04c82/src/cli/handlers/observation.ts#L37-L73","documentation":"ProcessRegistry persists managed-process records to a JSON registry file and reloads them at startup. JSON.parse of registryPath threw — the file is missing (readFileSync ENOENT is caught by the same block), truncated, or not JSON. The handler warns, clears entries, then prunes dead PIDs and persists a rebuilt registry, so the system self-heals at the cost of losing tracked runtime handles.","triggerScenarios":"First run where the registry file does not exist yet (ENOENT hits this catch); a crash during persist() leaving a partial write; two supervisors sharing one registryPath clobbering each other; manual edits or schema change between versions.","commonSituations":"Fresh install bootstrap; upgrading claude-mem versions that changed the persisted shape; process killed during shutdown while the registry was being written; users sharing a home directory between two instances.","solutions":["Safe to ignore on first run or one-off crashes — the registry rebuilds and pruneDeadEntries cleans stale rows.","If records keep getting lost, ensure a single writer per registry path (one supervisor instance).","Make persist() atomic (tmp file + renameSync) so crashes never leave truncated JSON.","Distinguish ENOENT from real corruption in the log path so bootstrap noise does not mask corruption."],"exampleFix":"// before\nconst raw = JSON.parse(readFileSync(this.registryPath, 'utf-8')) as PersistedRegistry;\n\n// after (missing file is normal bootstrap, not corruption)\nlet raw: PersistedRegistry;\ntry {\n  raw = JSON.parse(readFileSync(this.registryPath, 'utf-8')) as PersistedRegistry;\n} catch (error) {\n  const code = (error as NodeJS.ErrnoException).code;\n  if (code !== 'ENOENT') {\n    logger.warn('SYSTEM', 'Failed to parse supervisor registry, rebuilding', { path: this.registryPath });\n  }\n  raw = { processes: {} };\n}","handlingStrategy":"fallback","validationCode":"import { existsSync, readFileSync } from 'fs';\n\nfunction loadRegistrySafe(path: string): PersistedRegistry | null {\n  if (!existsSync(path)) return null; // first run — normal\n  try {\n    const parsed: unknown = JSON.parse(readFileSync(path, 'utf-8'));\n    if (isPersistedRegistry(parsed)) return parsed;\n  } catch {\n    /* corrupt */\n  }\n  return null; // caller rebuilds from scratch\n}","typeGuard":"function isPersistedRegistry(value: unknown): value is PersistedRegistry {\n  if (typeof value !== 'object' || value === null) return false;\n  const processes = (value as PersistedRegistry).processes;\n  return processes === undefined || (\n    typeof processes === 'object' && processes !== null &&\n    Object.values(processes).every(v =>\n      typeof v === 'object' && v !== null && typeof (v as ManagedProcessRecord).pid === 'number')\n  );\n}","tryCatchPattern":null,"preventionTips":["Treat the registry as a cache of PIDs, not source of truth — any record can vanish at any time by design.","Keep one writer per registry path; multiple supervisors corrupt each other's state.","Make persist() atomic (tmp + rename) to survive crashes mid-write.","Expect ENOENT on first boot and log it at debug so real corruption stands out."],"tags":["process-registry","json-parse","persistence","self-healing"],"backgroundTag":"corrupted-state-file","analyzedSha":"e2d1df569a8f04075d40e92461128ece7cf04c82","analyzedAt":"2026-08-20T23:58:13.836Z","schemaVersion":2},"datasetVersion":"2026-08-29T07:17:48.351Z"}