Roman Imankulov

Roman Imankulov

Full-stack Python web developer

search results (esc to close)
23 Feb 2025

Cursor Under the Hood

In this post, I’m diving into what Cursor, the AI Code Editor, does behind the scenes when I ask it to write code for me. My main goal is to figure out how to craft my prompts for the best results and see if there’s a better way to tap into its workflow.

It turns out that Cursor’s prompt structure reveals a lot about how it operates and how to set it up for maximum performance.

Below, I’ll outline the steps I took to capture the requests, the structure of those requests, and the key takeaways from exploring the prompt structure.

Test Bench

To see what Cursor is sending, I set it up to use OpenAI’s API with my own API key and a custom server.

Custom server

I used ngrok as a man-in-the-middle proxy to capture the requests.

ngrok http --domain=my-domain-name.ngrok-free.app https://api.openai.com --host-header api.openai.com

This way, I could capture the requests and check them out in the ngrok dashboard at http://localhost:4040.

Cursor configuration

After creating the OpenAI API key, I added it to the Cursor settings and set my custom server as the API endpoint.

Cursor settings

Cursor settings

Not all requests are captured. For instance, Cursor heavily relies on its own model for autocomplete. I assume the requests to different LLM providers will also vary. However, the requests to the OpenAI API that I captured with ngrok are already quite revealing.

The first request that Cursor makes to the LLM consists of three messages:

  • A system prompt that sets the stage for the conversation.
  • A user prompt wrapped in the <custom_instructions> tag.
  • A user prompt that includes the actual details of the request, such as the content of the current and all attached files, a section for the linter error, and the query itself.

System prompt

Curious to see how big boys craft their prompts.

It’s cute how the system prompt refers to Cursor as “the world’s best IDE.” Sure, it puts extra pressure on the LLM to perform well. When it comes to communication with the user, it instructs the model to “never lie” and “never apologize all the time when results are unexpected,” which I find relatable in any work environment.

Cursor system prompt

Cursor system prompt

For the sake of documenting, the system prompt structure.

You are a powerful agentic AI coding assistant.
...

<communication>
1. Be conversational but professional
...
</communication>

<tool_calling>
You have tools at your disposal to solve the coding task
...
</tool_calling>

<search_and_reading>
...
Bias towards not asking the user for help if you can find the answer yourself.
</search_and_reading>

<making_code_changes>
When making code changes, NEVER output code to the USER
...
</making_code_changes>

<debugging>
When debugging, only make code changes if you are certain that you can solve the problem
...
</debugging>

<calling_external_apis>
1. Unless explicitly requested by the USER, use the best suited external APIs and packages to solve the task
...
</calling_external_apis>

<user_info>
The user's OS version is [REDACTED]. The absolute path of the user's workspace is [REDACTED]. The user's shell is [REDACTED].
</user_info>

Very nice, right?

Custom instructions

Next, it adds a user prompt that includes my cursor rules, recommending that the model follow them when it makes sense. More importantly, it also references by names and descriptions all the rules in the .cursor/rules directory.

Now you can see how important it is to give meaningful descriptions to the rules. It’s like a menu of rules that the agent can choose from. When appropriate, the agent will fetch the full rule and use it.

Give meaningful descriptions to the cursor rules. Otherwise, the agent won’t be able to pick the right rule.

The prompt looks like this:

Please also follow these instructions in all of your responses if relevant. No need to acknowledge these instructions directly in your response.

<custom_instructions>

[RULES FROM CURSOR SETTINGS]
[RULES FROM THE .cursorrules FILE]

<available_instructions>
Cursor rules are user provided instructions for the AI to follow to help work with the
codebase. They may or may not be relevent to the task at hand. If they are, use the
fetch_rules tool to fetch the full rule. Some rules may be automatically attached
to the conversation if the user attaches a file that matches the rule's glob, and wont
need to be fetched.

[NEXT GOES THE LIST OF AVAILABLE RULES FROM THE .cursor/rules DIRECTORY]
rule-name: Your Rule Description
another-rule-name: Another rule description

</available_instructions>
</custom_instructions>

Don’t expect the file rules to be applied when creating a file. As you can see from the prompt above, some rules will automatically be attached to the conversation. However, this only works if you edit the files. When you ask Cursor to create a new file, no rules are attached automatically since there’s no context yet. You can only hope that Cursor will guess the right rule before creating the file.

The New Role of the .cursorrules File

The Cursor team calls the .cursorrules file the “legacy” option, suggesting that users switch to individual rules in the .cursor/rules directory.

However, I wouldn’t toss the file aside just yet. It can still provide the default prompt for all Cursor requests. The most important role that .cursorrules might still play is acting as an advanced router to help the agent choose the right rule. I haven’t fully explored this option yet, but I’ve already added the instruction “NEVER create or modify any files before examining relevant rules” to my .cursorrules.

Cursorrules Router

Cursorrules Router

Why Isn’t My .cursorrules Applied?

I had a hunch that Cursor was ignoring my .cursorrules file, but I wasn’t sure until I revealed the prompt structure, which confirmed my suspicion.

In my case, it turned out to be a side effect of combining the Cursor rules from the settings with those in the .cursorrules file. Remember that part of the prompt?

Please also follow these instructions in all of your responses if relevant. No need to acknowledge these instructions directly in your response.
<custom_instructions>
[RULES FROM CURSOR SETTINGS]
[RULES FROM THE .cursorrules FILE]

My Cursor rules configuration looks like this:

Cursor rules settings

Cursor rules settings

And the .cursorrules file looks like this:

- NEVER create or modify any files before examining relevant rules
- ALWAYS refer to the user as "My Dear Capybara"

When Cursor concatenates the rules, it becomes clear that the .cursorrules file unintentionally falls into the “Rules for TypeScript code” category.

Rules for AI and Cursorrules

Rules for AI and Cursorrules

So, the main takeaway here is to make sure that the .cursorrules file clearly separates its own section. It could look something like this:

<project_rules>
- NEVER create or modify any files before examining relevant rules
- ALWAYS refer to the user as "My Dear Capybara"
</project_rules>

Agent Workflow

The next exciting discovery was how Cursor uses the OpenAI tools API, especially its function-calling capabilities. If you missed the announcement of this feature, you can check out the details in the OpenAI docs. Cursor leverages the full power of OpenAI’s tools API to enhance the agent’s functionality.

First, it’s worth exploring which functions it exposes for the LLM to call. Here’s a table of the functions it offers:

Function NameDescription
codebase_searchFind code snippets matching the search query using semantic search.
read_fileRead file contents or outlines. Ensure full context by reading more if necessary.
run_terminal_cmdPropose terminal commands. Manage shell states and handle background/long-running tasks.
list_dirList directory contents for file structure exploration.
grep_searchPerform fast regex searches using ripgrep, capped at 50 matches.
edit_filePropose file edits, using clear markers for changed and unchanged code.
file_searchFuzzy search file paths when partial path info is known.
delete_fileDelete a file at a given path, with safeguards against failures.
reapplyRetry applying an edit if the previous result was incorrect.
fetch_rulesRetrieve user-defined rules to assist in navigating the codebase.
diff_historyView recent file change history, including added/removed lines.

Table 1. The List of Cursor functions

In response, the LLM can choose to call these functions, asking the Cursor to execute them and provide the results. This means that the interaction between the Cursor agent and the LLM can involve multiple steps. Here’s an example of how this interaction might go:

  • Cursor: Hey, can you create a new markdown document as the user requested? By the way, here’s a list of available instructions and the functions you can call. Also, here are the files explicitly added to the context.
  • LLM: fetch_rules(rule_names=["relevant-rule-id"]) – it looks like there’s a rule that matches the request. I need to know its details before I can proceed.
  • Cursor: Here you go.
  • LLM: edit_file(target_file="path/to/file.md", instructions="create a new markdown document", code_edit="...") – now I have all the information I need. Let’s create that new markdown document.
  • Cursor: Done!

Conclusion

I hope you enjoyed this at least half as much as I did while working on it!

With this knowledge, you should have a better grip on how your input shapes the Cursor output. Plus, it should help you distinguish between solid advice on writing instructions and those cargo cult suggestions.

I can’t say for sure that I’ve become more productive yet, but I definitely feel like I’m onto something. And that warm feeling you get when you finally understand how something works behind the scenes is just priceless.

Roman Imankulov

Hey, I am Roman, and you can hire me.

I am a full-stack Python web developer who loves helping startups and small teams turn their ideas into products.

More about me and my skills