Why do Cursor and Claude.ai produce different responses?

Hi. I’m relatively new to Cursor and to coding with LLMs, so I’m still trying to learn my way around and figure out how best to use them. I’m trying to understand why Cursor produces responses that are different from Claude.ai when they are both supposedly using the same model. Here’s an example of what I mean.

I start with the following function, which I believe was generated with GPT 4o (Claude was down at the time, so cursor fell back to a different model):

export function parseProperty<T extends z.ZodType>(
  obj: unknown,
  path: string,
  schema: T,
): z.infer<T> | null | undefined {
  const keys = path.split(".");
  let value: unknown = obj;

  for (const key of keys) {
    if (
      typeof value === "object" &&
      value !== null &&
      !Array.isArray(value) &&
      key in value
    ) {
      value = (value as Record<string, unknown>)[key];
    } else {
      return undefined;
    }
  }

  if (value === null) {
    return schema.safeParse(null).success ? null : undefined;
  }

  const parsed = schema.safeParse(value);
  return parsed.success ? parsed.data : undefined;
}

In Cursor I started a new chat with this file as context, and asked the following: “can you please look over the parseProperty method we have here and confirm that it has the correct logic and is written in the cleanest and most concise way?”

Here is the response from Claude:


I’ve reviewed the parseProperty function, and overall, it’s well-written and follows a logical approach. However, there are a few minor improvements we can make to enhance its clarity and conciseness. Here’s an updated version with explanations:

export function parseProperty<T extends z.ZodType>(
  obj: unknown,
  path: string,
  schema: T,
): z.infer<T> | null | undefined {
  const keys = path.split(".");
  let value: unknown = obj;

  for (const key of keys) {
    // Simplified type check
    if (value && typeof value === "object" && !Array.isArray(value)) {
      value = (value as Record<string, unknown>)[key];
    } else {
      return undefined;
    }
  }

  // Handle null case first
  if (value === null) {
    return schema.isNullable() ? null : undefined;
  }

  // Use schema.parse() instead of safeParse() for more concise code
  try {
    return schema.parse(value);
  } catch {
    return undefined;
  }
}

Explanations of the changes:

  • Simplified the type check in the loop:

  • Removed the explicit value !== null check, as typeof value === “object” already excludes null.

  • Moved the !Array.isArray(value) check earlier for better readability.

  • Used schema.isNullable() instead of schema.safeParse(null).success:

  • This is a more direct way to check if the schema allows null values.

  • Replaced the safeParse() logic with parse() and a try-catch block:

  • This simplifies the code and reduces the number of lines.

  • It still achieves the same result: returning the parsed value if successful, or undefined if parsing fails.

These changes make the function slightly more concise while maintaining its functionality. The logic remains correct, handling nested object properties, null values, and schema validation as intended.


So I pointed out that it might not be the best idea to remove the null check since typeof null is “object”, and also that replacing 2 lines at the end of the function with 5 lines using a try catch block is perhaps not really more concise, even though the lines are shorter. And then it proceeded to apologize and agree on both counts, and suggested undoing those changes.

Now, for comparison, I decided to go to the Claude website and see what kind of response it produces, expecting it to be fairly similar. I gave it the function for context and used the exact same prompt as I used in Cursor chat. Here was its response:


I’d be happy to review the parseProperty function for you. Let’s go through the logic and then consider if there are any improvements we can make.

Now, let’s analyze the function.

  1. The function signature looks correct. It takes an unknown object, a string path, and a Zod schema, and returns the parsed value, null, or undefined.
  2. The logic for traversing the object using the dot-separated path is correct. It splits the path into keys and iterates through them.
  3. The type checking for each nested object is thorough, ensuring it’s an object, not null, not an array, and has the key.
  4. The handling of null values is good, checking if the schema allows null values.
  5. The final parsing using the Zod schema is correct, using safeParse and returning the parsed data if successful, or undefined if not.

The logic appears to be correct. However, we can make a few improvements for cleanliness and conciseness:

  1. We can use optional chaining and nullish coalescing to simplify the nested object traversal.
  2. We can use type narrowing to avoid the explicit type assertion.
  3. We can combine the null check with the schema parsing for a more concise flow.

Here’s an improved version:

export function parseProperty<T extends z.ZodType>(
  obj: unknown,
  path: string,
  schema: T
): z.infer<T> | null | undefined {
  const keys = path.split(".");
  let value: unknown = obj;

  for (const key of keys) {
    if (typeof value !== "object" || value === null || Array.isArray(value)) {
      return undefined;
    }
    value = (value as Record<string, unknown>)[key];
  }

  return schema.safeParse(value).success ? schema.parse(value) : undefined;
}

Key improvements:

  1. Simplified the object traversal logic by inverting the condition and returning early if the value is not a valid object.
  2. Removed the explicit null check, as safeParse will handle null values according to the schema.
  3. Used schema.parse(value) instead of parsed.data to ensure the correct type is inferred.

This version is more concise while maintaining the same functionality. It’s also more robust, as it relies on Zod’s parsing to handle null values according to the schema, rather than having a separate check.

Would you like me to explain any part of this improved version in more detail?


These responses are significantly different, with the Claude.ai response being far more interesting and useful. So what is happening here? Why does Cursor produce such a different response to the Claude website, when both are using the same LLM?

On a somewhat related note, I’ve also noticed on several occasions when I ask a question in Cursor and provide the files as context, the response will contain things like this: “this code references function xyz, which probably looks something like this: …” In other words, it’s like even though I’ve provided the files with the relevant code as context and function xyz is right there for it to examine, it acts as if it has no idea about it and just makes an educated guess about what the function might look like. Why would this be?

Apart from those kinds of issues, I’m really enjoying Cursor, and the tab auto-complete is really useful and generally accurate at predicting what I’m trying to do. But with the chat, at times I feel like I’m burning through a lot of my 500 requests just going in circles with Claude because it’s being so difficult to work with, suggesting changes that don’t make sense, offering solutions that we already determined don’t work just a few prompts ago, acting like it can’t see all the context I’ve provided to it, etc.

Sorry for the long post, but curious to hear others’ thoughts.

1 Like

To research this further you would probably need automated testing software to repeat a few trials of the exact same state of the cursor local installation to check a few levels on what is causing the output to be what it is.

At first level: to see how the (obtained outside the frozen state) temperature of the model varies the model output. Same would go for the Claude API.

At another level: to see the RAG pipeline used in matching (and reranking) the relevant document chunks, and it wouldn’t be surprising if a separate model(s) temperature is affecting the return here. The return may or may not contain the function that is missing depending on whatever this pipeline is doing.

As an extreme example of another layer that may be separate entirely from the base rag pipeline: context returned/ to-user-output returned may vary based on censoring-level filtering, which is obviously in place from just about every provider. Not that there is anything disagreeable in normal computer code, but as most providers try to censor their output for safety (safety first), then your an assume that you need to ablate a censoring layer as well.

I noticed same in composer when using all folder as ‘@’ mention (folder of many scripts though, so I possibly just overwhelm it and the context cannot see the scripts it’s about to edit. what’s funny in my case though it edited the file which it complained about not having it :smiley: , probably because it didn’t have it in context when started responding but then mechanisms of composer itself found the file for editing and gave it for a new iteration and then it edited the file, just a theory of how it could possibly happen). So Claude produces better code at the moment than same Claude through Cursor? which model, you used the sonnet-3.5 in both attempts? (in claude where, in workbench on the website?)

I keep trying to squeeze some useful and valid typescript in a medium sized 2d game project demo, from o1-mini, but no luck so far, the small snippets by themselves are ‘valid’, but as a whole, a high level task it was given across files, at the project level, it always a failure (even a simple thing like make player sprite not only move towards the direction it has to go, but also rotate into that direction first and only after that start moving there) so it combines bunch of snippets together into a “maybe it should be the right answer to what user wants?” salad, without understanding the project classes and methods and their relations, in reality, so results are mostly wrong, even over 5-6 iterations. But now I have an idea to run sonnet on this more, instead of o1-mini. I was pretty sure o1-mini is the best choice now, because of benchmarks it shown.