Docs · Working with Cursor

Building a Feature in Cursor

Plan mode, the first pass, and the two files to read carefully in every diff.

The editor loop is the tightest of the agent loops, because you never leave the file and you see every change in place before you accept it. The tradeoff is that accepting is one keystroke, which pulls the work toward many small edits. That is excellent for refining a feature and worse for building a whole one, where you want the model, the policy, the controller, the screens and the tests to arrive as one coherent set. This page is the loop that works around that.

Start in Plan mode

Press Shift and Tab from the chat input to rotate modes, or pick Plan from the mode dropdown in Agent. The agent asks clarifying questions, reads the codebase, and writes an implementation plan before it writes any code. You review and edit that plan as markdown, then build it when it looks right.

This is worth the extra step for a whole feature, because the plan is where a wrong assumption is cheap to fix. The two assumptions to check every time:

  • Is the resource account-scoped? It should be, and the plan should say so. Everything holding customer data in this app belongs to an Account. See Accounts and Tenancy.
  • Is the feature paid or free? This is the one input no agent can infer from a feature description, and it decides whether the controller includes RequireEntitlement. Say which one you want in the first message. See Gating a Feature.

Plans are saved to your home directory by default. Click "Save to workspace" to move a plan into the repository, which is what you want if the feature spans more than one session or another person will pick it up.

The first pass

Name the shape rather than describing it:

Add a Bookmarks resource following the same shape as the example Project slice:
model, policy, controller, views, routes, and specs. Free, not gated.

"The same shape as the example Project slice" points at files that already exist, which is far more precise than any description you could type, and it is exactly what crud-with-billing does if you invoke the skill directly instead. Either route produces the same thing. See Adding a Resource.

What comes back should touch the model, the migration, the policy, the controller, the views, the route, and three spec files.

Read two files properly, skim the rest

The migration. It must have t.references :account, null: false, foreign_key: true. A missing account_id is the one mistake in this codebase that is expensive to fix later, because by then there are rows.

The policy. It must inherit the account-filtering Scope, and the controller must call policy_scope on collections and authorize on individual records. Look at app/policies/project_policy.rb next to it if you are unsure what correct looks like. See Authorization.

Everything else, skim. Views and route lines are cheap to fix and obvious when wrong.

The habit worth building here is to look at what the diff touched, not only what the summary says. The summary describes intent; the file list describes blast radius. When they disagree, believe the file list.

Verify before you accept a run of edits

bin/check

RSpec, RuboCop, Brakeman, bundler-audit, the importmap audit, and a secret scan. Green is the definition of done. Run it after a run of edits rather than after a dozen, because the tight loop makes it genuinely easy to get twelve edits deep, and then the failure could be from any of them.

One spec is worth opening by hand rather than trusting the summary: the tenant-isolation test in the generated request spec, where a signed-in user asking for another account's record gets a 404. If it is missing, the resource is not finished. See Verifying Your Setup for what each tool in bin/check catches.

Two ways this goes wrong in an editor specifically

Context you did not intend. Cursor reads the files you have open along with the ones it decides are relevant. If a suggestion is oddly shaped, check your tabs before blaming the model. The flip side is the cheapest trick available: open the existing example of the thing you are building in another tab before you ask.

Small steps hiding structural drift. Twelve good local edits can arrive at a structure nobody would have chosen deliberately. Every so often, read the whole feature as one thing rather than as the diff you last accepted.

The failures that produce no error at all

Three of them, all caused by the enforced Content Security Policy, and all invisible: the button just does nothing.

  • A form that redirects to an external host needs that host in policy.form_action in config/initializers/content_security_policy.rb, because form-action covers the whole navigation a form starts, including the redirect.
  • That same form needs data-turbo="false", on the form or the submit button, or Turbo submits it with fetch and tries to follow the cross-origin redirect as an XHR, which connect-src blocks. Both conditions must hold.
  • An inline <script> in a view is dropped, because script tags carry a nonce. Interactive behaviour goes in app/javascript/controllers/<name>_controller.js, which auto-registers with no importmap edit and no build step.

The controllers.mdc rule described in Rules for One Shot exists to put all three in front of the agent whenever it touches a controller, a view, or a Stimulus controller.

Next

You have the whole loop. Move on to publishing: Writing a Blog Post.