Most developers write prompts like they write scripts. Step one, step two, step three. It feels natural — we've been writing imperative code for most of our careers.
But an LLM isn't an interpreter. It's closer to a smart runtime you configure. The prompts that actually work read like a spec, not a recipe. And yet most of us — me included, for a long time — keep reaching for the imperative reflex.
A quick refresher
Imperative code tells the machine how: loop over this array, mutate that variable, branch on this condition.
Declarative code tells the machine what: give me all users who signed up last week, render a component tree that looks like this, keep three replicas of this pod alive.
SQL is declarative. Terraform is declarative. Kubernetes is declarative. Every tool from the last decade that removed mental load from the developer moved in this direction. There's a reason for that.
The imperative prompt trap
Here's an example:
First, read config/routes.rb. Then open the ArticlesController.
Find the show action. Add a before_action for authentication.
After that, update the view to hide the edit button if the user
isn't an admin.
That's not a prompt. That's a commit message written in advance. It reads like someone narrating what they'd do if they were driving the keyboard themselves.
The problem is that the moment reality doesn't match the script, the model is stuck. What if the before_action is already there? What if the view uses a partial? What if "admin" is a role on a different model, or a method on Current.user we've been using for months?
You wrote a script. The model follows scripts faithfully, even when they're wrong.
What declarative actually looks like
A good prompt states the outcome, the constraints, and what "done" looks like:
Only admins should be able to edit an article. Non-admins should
see the page but not the edit button. Stay consistent with how we
handle authentication and authorization elsewhere in this app.
No steps. No file paths. No sequencing. The model figures out the path. It reads the routes file because it needs to, not because I told it to. It finds the partial, the concern, the helper — the things I didn't know existed — because I told it what done looks like, not how to get there.
You're not programming the model. You're configuring the context it reasons inside.
That one reframing changed more of my prompts than any "prompt engineering" tip I've ever read.
CLAUDE.md, skills, MCP — same story
Everything you write around the model follows the same rule.
A good CLAUDE.md is a spec. "Rails 8.1, Ruby 3.4, SQLite in production, fat models thin controllers, Minitest with fixtures, session auth without Devise." The model reads that, plus the code, plus the current request, and decides what to do.
A bad CLAUDE.md is a runbook. "First check the Gemfile. Then open config/database.yml. Then look at the models directory." Every line is a wasted line, because the model would have read those files anyway the moment it needed to.
Skills are the same. The best skills I've written describe the job: here's the goal, here's the voice I'm going for, here's what "done" looks like, here's what to avoid. They don't describe a sequence of steps. A skill that says "first draft an outline, then write the intro, then the body, then the summary" is writing for a compiler, not a writer.
Sub-agent descriptions, MCP server docs, prompts inside a pipeline — same thing. You tell the model what to aim for, not how to land.
When imperative still wins
Imperative isn't wrong. It's just in the wrong place.
The harness is imperative. The hook that runs on PreToolUse. The tool that opens a browser and fills in a form. The pre-commit check that runs Rubocop and blocks the commit if it fails. That's code. It should be deterministic, step-by-step, testable, and boring.
The split is clean:
- Harness: imperative. Code, hooks, tools, deterministic workflows.
- Context: declarative. Prompts, CLAUDE.md, skills, sub-agent descriptions.
When you blur the two, things get weird. Imperative prompts turn the model into a stubborn robot that follows a bad script. Declarative hooks turn your harness into a guessing machine that silently "interprets" what you meant. Keep them in their lanes.
Summary
Imperative thinking is hard to shake — we spent most of our careers writing scripts, so of course our prompts look like scripts. But the model is a different kind of runtime. It wants a spec, not a recipe. Describe the outcome, the constraints, and what "done" looks like, and trust the runtime to find the path. Save imperative for the harness, where determinism actually pays off.