thedotmack/claude-mem · error · AdapterRejectedInput

adapter rejected input: invalid_cwd

Error message

adapter rejected input: invalid_cwd

What it means

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.

Solutions

  1. Ensure Cursor sends a valid workspace_roots array or cwd in the hook payload
  2. Update the adapter if Cursor changed its hook schema (check version compatibility)
  3. Remove stale empty-string entries from workspace_roots before invoking
  4. Catch AdapterRejectedInput with .reason === 'invalid_cwd' and retry with an explicit cwd

Example fix

// before
cursorAdapter.normalizeInput({ workspace_roots: [null], cwd: null });
// after
cursorAdapter.normalizeInput({ cwd: '/absolute/path/to/workspace' });
Defensive patterns

Strategy: validation

Validate before calling

const cwd = raw?.workspace_roots?.[0] || raw?.cwd || process.cwd();
if (typeof cwd !== 'string' || cwd.length === 0) throw new Error('no usable cwd for cursor event');

Type guard

function hasValidCwd(v: unknown): v is string { return typeof v === 'string' && v.length > 0; }

Try / catch

try { adapter.normalizeInput(raw); } catch (e) { if (e instanceof AdapterRejectedInput && e.reason === 'invalid_cwd') { /* retry with explicit cwd */ } else throw e; }

Prevention

When it happens

Trigger: 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).

Common situations: 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].

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17). Data as JSON: /api/errors/b84bccc9f704118f. Report an issue: GitHub.

Appendix: source

Thrown at src/cli/adapters/cursor.ts:36

// set so a malicious sessionId from stdin cannot escape ~/.cursor/projects via
// path separators, '..' segments, or null bytes (security review on PR #2282).
const SAFE_SESSION_ID_RE = /^[A-Za-z0-9_-]+$/;

export function deriveCursorTranscriptPath(cwd: string | undefined, sessionId: string | undefined): string | undefined {
  if (!cwd || !sessionId) return undefined;
  if (!SAFE_SESSION_ID_RE.test(sessionId)) return undefined;
  const slug = cwd.replace(/^\//, '').replace(/[/.]/g, '-');
  const candidate = join(homedir(), '.cursor', 'projects', slug, 'agent-transcripts', sessionId, `${sessionId}.jsonl`);
  return existsSync(candidate) ? candidate : undefined;
}

export const cursorAdapter: PlatformAdapter = {
  normalizeInput(raw) {
    const r = (raw ?? {}) as any;
    const isShellCommand = !!r.command && !r.tool_name;
    const cwd = r.workspace_roots?.[0] || r.cwd || process.cwd();
    if (!isValidCwd(cwd)) {
      throw new AdapterRejectedInput('invalid_cwd');
    }
    const sessionId = r.conversation_id || r.generation_id || r.id;
    return {
      sessionId,
      cwd,
      prompt: r.prompt ?? r.query ?? r.input ?? r.message,
      toolName: isShellCommand ? 'Bash' : r.tool_name,
      toolInput: isShellCommand ? { command: r.command } : r.tool_input,
      toolResponse: isShellCommand ? { output: r.output } : r.result_json,  // result_json not tool_response
      toolUseId: typeof r.tool_use_id === 'string' ? r.tool_use_id : (typeof r.tool_call_id === 'string' ? r.tool_call_id : undefined),
      // Cursor's stop hook does not pass a transcript path on stdin, but it
      // does write a JSONL transcript to disk under ~/.cursor/projects/...,
      // so we derive the path from cwd + conversation id.
      transcriptPath: deriveCursorTranscriptPath(cwd, sessionId),
      filePath: r.file_path,
      edits: r.edits,
    };
  },

View on GitHub (pinned to d8bc9755e7)