Core Answer
A productive Vibe Coding session is not "prompt until the preview looks right." It is a controlled loop:
baseline -> task contract -> minimal context -> bounded change
-> inspect diff -> run evidence -> attack edge cases
-> accept, revise, or revert
This guide runs that loop on a real Go feature. The example is deliberately small, deterministic, and framework-free so you can reproduce it in Cursor, Windsurf, TRAE, Claude Code, or another coding agent without turning tool behavior into the lesson.
If you need the definition and risk boundary first, read What Is Vibe Coding?. For a detailed product comparison, use the Vibe Coding tools evaluation guide.
The Exercise: Summarize Transactions Safely
The task is to add a package that summarizes signed transaction amounts:
- positive amounts are income;
- negative amounts are spending;
- spending is grouped by normalized category;
- zero amounts, blank categories, oversized amounts, and oversized batches are rejected;
- the function must not mutate its input;
- all money uses integer cents, never floating point.
This is more useful than a visual demo because correctness has an observable oracle. The same fixture can compare tools, prompts, and model versions.
Step 0: Establish the Baseline
Before asking an agent to edit anything:
git status --short
go test ./...
go vet ./...
Record the commit SHA and command results. If the baseline is already failing, identify those failures before the generated change. Otherwise the agent can claim an existing failure as its own regression or "fix" unrelated code.
Use an isolated branch or worktree. Do not expose production credentials, customer data, signing keys, or unrestricted cloud access merely because the task is small.
Step 1: Write the Acceptance Contract
Give the agent a contract, not a mood:
Goal:
Add internal/summary with a pure Summarize function.
Inputs:
[]Transaction{Category string, AmountCents int64}
Required behavior:
positive = income; negative = spending;
trim category whitespace;
aggregate spending by normalized category;
reject zero, blank category, abs(amount) > 1e12 cents,
or more than 10,000 transactions;
do not mutate input.
Out of scope:
HTTP, persistence, currency conversion, dependencies, refactors.
Evidence:
gofmt, go test ./..., go vet ./...;
focused tests for valid, invalid, boundary, and input-mutation cases.
Escalate:
any new dependency, public API change outside the package,
ambiguity in signed-amount semantics, or need for external access.
This contract does four jobs: it fixes semantics, limits the blast radius, defines what success means, and tells the agent when not to guess.
Step 2: Provide Minimal, Trusted Context
Ask the agent to inspect:
go.mod;- one nearby package that shows naming and error conventions;
- the repository's real test and lint commands;
- the host's project instruction file.
Do not paste the entire repository into a chat by default. More context can increase cost, bury the relevant contract, and expose unrelated data.
Use the current instruction mechanism for your host:
| Host | Current project context |
|---|---|
| Cursor | .cursor/rules/*.mdc or AGENTS.md |
| Windsurf | .windsurf/rules/*.md or AGENTS.md |
| Claude Code | CLAUDE.md plus .claude/settings.json for shared permissions |
| TRAE | Project Rules and Skills configured for the workspace |
Instruction files shape model context. They do not enforce security policy. Put formatting and architectural conventions there; put access control in the executor, sandbox, CI, and server.
Step 3: Ask for a Plan Before an Edit
Use a prompt such as:
Read the acceptance contract and relevant repository conventions.
Before editing, report:
1. files you will add or change;
2. invariants each test will prove;
3. commands you will run;
4. assumptions or ambiguities.
Do not edit outside internal/summary.
Reject the plan if it adds a database, HTTP handler, third-party decimal package, or unrelated refactor. A plan is useful only when it exposes scope before the diff exists.
Step 4: Implement the Smallest Correct Change
A valid implementation can look like this:
package summary
import (
"fmt"
"strings"
)
const (
maxTransactions = 10_000
maxAbsAmountCents = int64(1_000_000_000_000)
)
type Transaction struct {
Category string
AmountCents int64
}
type Summary struct {
IncomeCents int64
SpendingCents int64
ByCategory map[string]int64
}
func Summarize(transactions []Transaction) (Summary, error) {
result := Summary{ByCategory: make(map[string]int64)}
if len(transactions) > maxTransactions {
return Summary{}, fmt.Errorf("too_many_transactions")
}
for index, transaction := range transactions {
category := strings.TrimSpace(transaction.Category)
amount := transaction.AmountCents
if category == "" {
return Summary{}, fmt.Errorf("transaction_%d_category_required", index)
}
if amount == 0 {
return Summary{}, fmt.Errorf("transaction_%d_amount_required", index)
}
if amount < -maxAbsAmountCents || amount > maxAbsAmountCents {
return Summary{}, fmt.Errorf("transaction_%d_amount_out_of_range", index)
}
if amount > 0 {
result.IncomeCents += amount
continue
}
spending := -amount
result.SpendingCents += spending
result.ByCategory[category] += spending
}
return result, nil
}
The code is not correct because it looks idiomatic. It is correct only if the contract's behavior is demonstrated.
Step 5: Make the Evidence Executable
Ask for tests at the same time as the implementation, then read the assertions:
package summary
import (
"reflect"
"strings"
"testing"
)
func TestSummarize(t *testing.T) {
input := []Transaction{
{Category: "Salary", AmountCents: 250_000},
{Category: " Food ", AmountCents: -2_500},
{Category: "Food", AmountCents: -1_500},
}
original := append([]Transaction(nil), input...)
got, err := Summarize(input)
if err != nil {
t.Fatalf("Summarize() error = %v", err)
}
want := Summary{
IncomeCents: 250_000,
SpendingCents: 4_000,
ByCategory: map[string]int64{"Food": 4_000},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("Summarize() = %#v, want %#v", got, want)
}
if !reflect.DeepEqual(input, original) {
t.Fatalf("Summarize() mutated input: %#v", input)
}
}
func TestSummarizeRejectsInvalidTransactions(t *testing.T) {
tests := []struct {
name string
transaction Transaction
errorCode string
}{
{name: "blank category", transaction: Transaction{AmountCents: 1}, errorCode: "category_required"},
{name: "zero amount", transaction: Transaction{Category: "Food"}, errorCode: "amount_required"},
{name: "oversized amount", transaction: Transaction{Category: "Food", AmountCents: 1_000_000_000_001}, errorCode: "amount_out_of_range"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := Summarize([]Transaction{test.transaction})
if err == nil || !strings.Contains(err.Error(), test.errorCode) {
t.Fatalf("Summarize() error = %v, want code %q", err, test.errorCode)
}
})
}
}
func TestSummarizeRejectsOversizedBatch(t *testing.T) {
_, err := Summarize(make([]Transaction, maxTransactions+1))
if err == nil || !strings.Contains(err.Error(), "too_many_transactions") {
t.Fatalf("Summarize() error = %v, want too_many_transactions", err)
}
}
Run the repository's actual gates:
gofmt -w internal/summary
go test ./...
go vet ./...
git diff --check
Do not accept "tests should pass" as evidence. Capture the command, exit status, and relevant output. If a check cannot run, record why.
Step 6: Review the Diff, Not the Explanation
Inspect:
git diff --stat
git diff -- internal/summary
git status --short
Review against the contract:
- Did the agent touch only the declared scope?
- Do integer cents preserve money semantics?
- Are bounds strong enough to prevent aggregate overflow under the batch limit?
- Do tests prove output values rather than merely checking
err == nil? - Is input immutability actually tested?
- Did any dependency, generated file, or configuration change?
The agent's summary is an index into the diff, not proof about the diff.
Step 7: Run an Adversarial Pass
Ask a fresh reviewer, human or model, to challenge the accepted implementation without editing it:
Review only against the acceptance contract.
Look for untested boundaries, overflow, mutation, ambiguous category
normalization, hidden scope changes, and assertions that can pass for
the wrong reason. Cite exact lines. Do not propose unrelated refactors.
For this example, useful follow-ups include exactly 10,000 transactions, 10,001 transactions, -1_000_000_000_000, Unicode whitespace policy, and whether category matching should be case-sensitive. The last item is a product decision, not something the model should silently invent.
Step 8: Preserve a Delivery Record
A reviewable handoff should contain:
Goal: transaction summary package
Base: <commit SHA>
Changed: internal/summary/summary.go, summary_test.go
Evidence: gofmt; go test ./...; go vet ./...; git diff --check
Assumptions: categories are case-sensitive; currency is pre-normalized
Skipped: load benchmark, because max batch size bounds runtime
Rollback: revert the isolated commit; no migration or external write
This record makes the result reproducible after the chat transcript is gone.
Recovery Rules When the Agent Loops
Do not keep adding prompts to a confused session indefinitely:
- stop after the same failure repeats twice;
- return to the last passing commit;
- reduce the task to one violated invariant;
- provide the exact failing command and minimal relevant files;
- switch model or take over if the next attempt repeats the failure.
Long conversations accumulate stale assumptions. A clean retry from a known state is often cheaper than repairing a polluted context.
A Reusable Completion Checklist
- [ ] Clean worktree and passing baseline recorded.
- [ ] Goal, scope, non-goals, invariants, evidence, and escalation written.
- [ ] Context is minimal, trusted, and free of secrets.
- [ ] Agent plan matches the declared files and behavior.
- [ ] Diff is small enough to understand completely.
- [ ] Tests assert valid, invalid, boundary, and mutation behavior.
- [ ] Real format, test, vet, lint, and build commands have run.
- [ ] Permission, dependency, and external-write changes were reviewed.
- [ ] Assumptions, skipped checks, and rollback are documented.
- [ ] A responsible human owns the result.
Primary Sources
- Cursor Rules documentation
- Windsurf Memories and Rules documentation
- Claude Code settings and permission scopes
- TRAE Rules documentation
- NIST Secure Software Development Framework
- OWASP Secure Coding Practices for AI-assisted Development
Conclusion
The durable skill in Vibe Coding is not composing a magical prompt. It is turning intent into a bounded contract, giving an agent only the context and authority it needs, and deciding from executable evidence. The faster generation becomes, the more important this control loop becomes.
Next, use Vibe Coding Best Practices to extend the same discipline from one task to team-wide security, governance, and measurement.