How I fixed my skill issue with Cursor
I had trouble being productive in Cursor until I focused on setting the LLM up for success.
I’ve noticed a trend on software engineering forums over the past year.
Person A declares how productive they are with LLMs. The statement will be something like, “Cursor has accelerated my coding. I coded an entire app last night. I can’t imagine doing so much without it.”
Person B responds, incredulous. “What kind of work do you do? Whenever I use it for anything, the output is unusable. I delete everything it does. Any improvement is incremental at best.”
I used to be a Person B when it came to Cursor. I’ve been improving with a lot of effort. My goal is to explain to other Person Bs a few mindset shifts I needed to get more from these tools. If you are a fellow Person B, frankly I don’t care if you use Cursor or similar tools. I think that generative coding will become more important as time goes on because of the rapid rate of improvement of the tools, but if you disagree that’s fine and maybe you’re right. I just want you to understand that these are skills that you can learn and improve, even if you think they’re not worth using now. After all, they won’t suddenly start getting worse over time. And so far they’ve just kept improving.
Here are the four mindset shifts I needed to work effectively with Cursor, presented without context. I wouldn’t take them too seriously at this point. They will either seem obvious or vague without the explanations below. But here they are:
Break tasks down.
Ruthlessly curate context.
Give a specific and concise prompt.
Accept and fix.
If you want an even shorter answer:
LLMs need pathological levels of hand holding to produce good work.
When Hinge — my employer — got a Cursor subscription, I signed up and immediately tried to use it. And I could not get it to work to my satisfaction! Every chat I had with the sidebar was frustrating. Every inline prompt fell on its face. How can people generate entire apps in a single day and I can’t get it to do anything interesting in a large codebase?
For at least a month, the only feature that worked reliably is Cursor’s tab-completion feature. Tab completion works great and is an obvious improvement over Github Copilot. I got interesting results on toy projects using inline prompting and the composer pane, but failed on a full production codebase. Quite honestly, I gave up for a while once a few prompts had gone poorly. I tried watching a few YouTube videos. These were terrible and either explained features that are in its manual or were like “just type that you want an app and it generates an app lmao.” Finally I had some conversations with people who used the tool extensively and asked, “when you work with Cursor, what are you spending all of your time doing?” and from there I was able to bootstrap my understanding.
I was surprised that I had trouble with Cursor. I was an early adopter of coding with LLMs. I’ve had a Github Copilot subscription since it launched. I also regularly used Claude or ChatGPT to accomplish rote tasks like “write the code that populates the fields in Golang struct B from struct A” or “Given this grpcurl
command line and this protobuf file, give me an example grpcurl
command line for this other endpoint.”
And here’s the reality: I was an F-Tier Cursor user. I assumed that I could command it like a magic box and it would always do the right thing. Because magic! But I never accepted the obvious: LLMs are as good as their data and their inputs, and my inputs were terrible.
As a reminder, here are the four mindset shifts that improved my ability to work with Cursor:
Break tasks down.
Ruthlessly curate context.
Give a specific and concise prompt.
Accept and fix.
Let’s look at each of them in turn.
Break tasks down
Let’s say that you want to build an in-memory key-value store. This is just an example; it can be anything. You don’t know the exact shape the API should take, but you have a rough idea.
Your first instinct might be to tell the LLM “generate a key-value store.” But this has problems for a few reasons:
This asks the LLM to solve several problems at once. Any errors will compound.
You will need to examine the solution to several problems at once. You might miss something.
It might generate a lot of code, which will be hard to review.
And even more importantly, “asking the LLM to do too much” is a common LLM failure case. This isn’t just for Cursor; it’s just how these things break. Cursor isn’t any different.
So how might we break down the key-value store? Generate the method signatures, get them right, and then fill the code in. First, you would give it a prompt like, “Write a key-value store object called KeyValueStore. Produce empty implementations for the methods.” Look at the output. If it’s anywhere close to right, just accept it and start modifying it to your satisfaction.
I just asked Cursor to do this, and it produced a Golang struct with methods named Get
, Set
, Delete
, Clear
, Keys
, Values
, and Size
. I can decide some things are missing (let’s add SetMany
and GetMany
) or things are wrong (let’s delete Keys
and Values
). Don’t do this within the chat session, just modify it by hand. Once you are satisfied with the API, then tell it to write all of the methods. Then accept and fix whatever it produces. Ask it to write tests for each of the functions and modify them as you see fit.
This isn’t the right size for every task. The more you work with it, the more you’ll develop a feel for this.
Ruthlessly curate context
This was my biggest skill issue with Cursor. I simply did not do a good job of curating the context.
Imagine that most of your requests to Cursor are trying to replace 1 to 10 minutes of coding1. Then it’s worth taking a little extra time giving Cursor the exact information that it needs.
Why is context important? Scroll back to the first time I mention the four mindset shifts I needed to use Cursor. Was that a good introduction to them? Did it help when I presented them without context? Were they actionable? The answer to all of these questions is obviously “no.” Context is crucial for comprehension and it is crucial for producing good work.
The LLM is not magic. You can drown it in context too. But if you don’t give it enough context, it will fill in the blanks itself. So take a little time. @-include the right files, highlight the part that needs to be modified when doing inline prompting, add any documentation that seems helpful, etc. Each time you execute a step and re-prompt it, consider whether the context should be changed. Give it an example if possible.
Give a specific and concise prompt
This one is pretty clear. Be specific, otherwise the LLM will fill in ambiguities or missing information itself. Be concise, because your own time is valuable and it gives the LLM less to misinterpret.
Accept and fix
I’ve seen this be a hangup for some developers. They break the problem down. They give it context. They write clear and obvious instructions. And then they see that the generated code is still obviously wrong!
There’s a great temptation to argue with it. “The inner loop of this function is wrong. You are computing blah blah blah and as I clearly stated it needs to do blah blah blah. Please correct your wrongs.” And then it generates different code with the same error just to irritate you. Don’t fall for this trap. Just accept and edit.
Don’t focus on getting Cursor’s output exactly right. Just fix it by hand. The tab completion feature will help you.
In conclusion: these are just baby steps
Generative and agent-based coding is still new. I expect that we will learn more about these tools over time, the tools will improve over time, and the workflow will become better and better until they become the new de facto way of working. But you can only benefit from the improvements if you have a working understanding of them. If you know how to work with them, then you will improve as the tools and methods use.
If it’s higher, then this point is even more important. If it’s less, then why are you trying to use LLM prompting? Just start writing and use the tab completion.