{"record":{"id":"b84bccc9f704118f","repo":"thedotmack/claude-mem","slug":"adapter-rejected-input-invalid-cwd-cursor","errorCode":null,"errorMessage":"adapter rejected input: invalid_cwd","messagePattern":"adapter rejected input: invalid_cwd","errorType":"validation","errorClass":"AdapterRejectedInput","httpStatus":null,"severity":"error","filePath":"src/cli/adapters/cursor.ts","lineNumber":36,"sourceCode":"// set so a malicious sessionId from stdin cannot escape ~/.cursor/projects via\n// path separators, '..' segments, or null bytes (security review on PR #2282).\nconst SAFE_SESSION_ID_RE = /^[A-Za-z0-9_-]+$/;\n\nexport function deriveCursorTranscriptPath(cwd: string | undefined, sessionId: string | undefined): string | undefined {\n  if (!cwd || !sessionId) return undefined;\n  if (!SAFE_SESSION_ID_RE.test(sessionId)) return undefined;\n  const slug = cwd.replace(/^\\//, '').replace(/[/.]/g, '-');\n  const candidate = join(homedir(), '.cursor', 'projects', slug, 'agent-transcripts', sessionId, `${sessionId}.jsonl`);\n  return existsSync(candidate) ? candidate : undefined;\n}\n\nexport const cursorAdapter: PlatformAdapter = {\n  normalizeInput(raw) {\n    const r = (raw ?? {}) as any;\n    const isShellCommand = !!r.command && !r.tool_name;\n    const cwd = r.workspace_roots?.[0] || r.cwd || process.cwd();\n    if (!isValidCwd(cwd)) {\n      throw new AdapterRejectedInput('invalid_cwd');\n    }\n    const sessionId = r.conversation_id || r.generation_id || r.id;\n    return {\n      sessionId,\n      cwd,\n      prompt: r.prompt ?? r.query ?? r.input ?? r.message,\n      toolName: isShellCommand ? 'Bash' : r.tool_name,\n      toolInput: isShellCommand ? { command: r.command } : r.tool_input,\n      toolResponse: isShellCommand ? { output: r.output } : r.result_json,  // result_json not tool_response\n      toolUseId: typeof r.tool_use_id === 'string' ? r.tool_use_id : (typeof r.tool_call_id === 'string' ? r.tool_call_id : undefined),\n      // Cursor's stop hook does not pass a transcript path on stdin, but it\n      // does write a JSONL transcript to disk under ~/.cursor/projects/...,\n      // so we derive the path from cwd + conversation id.\n      transcriptPath: deriveCursorTranscriptPath(cwd, sessionId),\n      filePath: r.file_path,\n      edits: r.edits,\n    };\n  },","sourceCodeStart":18,"sourceCodeEnd":54,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/src/cli/adapters/cursor.ts#L18-L54","documentation":"cursorAdapter.normalizeInput throws AdapterRejectedInput('invalid_cwd') when the cwd chosen from r.workspace_roots[0], r.cwd, or process.cwd() is not a non-empty string. Cursor typically supplies workspace_roots, so failure means those were empty/invalid and no usable fallback existed.","triggerScenarios":"Cursor hook payload with workspace_roots: [] and cwd missing (process.cwd() fallback should save this, so concrete failure is workspace_roots[0] or r.cwd being an empty string / non-string that the || chain still selects, e.g. workspace_roots[0] === '' short-circuits only on falsy — actually falsy values skip, so failure implies process.cwd() unavailable or a non-string falsy-skipped chain leaving an invalid value).","commonSituations":"Cursor extension updates changing the payload shape (workspace_roots renamed); global hooks run where the working directory was deleted; payload JSON with \"workspace_roots\": [null].","solutions":["Ensure Cursor sends a valid workspace_roots array or cwd in the hook payload","Update the adapter if Cursor changed its hook schema (check version compatibility)","Remove stale empty-string entries from workspace_roots before invoking","Catch AdapterRejectedInput with .reason === 'invalid_cwd' and retry with an explicit cwd"],"exampleFix":"// before\ncursorAdapter.normalizeInput({ workspace_roots: [null], cwd: null });\n// after\ncursorAdapter.normalizeInput({ cwd: '/absolute/path/to/workspace' });","handlingStrategy":"validation","validationCode":"const cwd = raw?.workspace_roots?.[0] || raw?.cwd || process.cwd();\nif (typeof cwd !== 'string' || cwd.length === 0) throw new Error('no usable cwd for cursor event');","typeGuard":"function hasValidCwd(v: unknown): v is string { return typeof v === 'string' && v.length > 0; }","tryCatchPattern":"try { adapter.normalizeInput(raw); } catch (e) { if (e instanceof AdapterRejectedInput && e.reason === 'invalid_cwd') { /* retry with explicit cwd */ } else throw e; }","preventionTips":["Ensure Cursor payloads contain workspace_roots or cwd","Track Cursor extension version changes to the hook schema","Strip null/empty entries from workspace_roots before invoking"],"tags":["input-validation","adapter","cwd","cursor"],"backgroundTag":"invalid-argument-value","analyzedSha":"d8bc9755e74915e5c3b999181e10a67c889bce2a","analyzedAt":"2026-09-17T16:40:26.182Z","contentChangedAt":"2026-09-17T16:40:26.182Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}