If you're working with coding agents like Claude Code, GitHub Copilot, or any other AI assistant, you've probably noticed something: they can generate code faster than you can review it. This creates a new set of challenges that traditional development workflows weren't designed to handle.
Here is what I believe will help you. BTW before reading this post try to:
Keep that branch around, once you've read the post, you'll go back and redo the same task with the principles below applied, so you can compare the two results yourself.
Your choice of tools depends on your experience level, and that choice shapes the whole workflow you're about to build:
Whichever you pick, you need to learn to be patient with LLMs. They usually tend to generate a ton of text, and rushing past it defeats the point of reviewing it at all.
When you code, you have a ton of context in your head. So if you are expecting a coding agent to deliver good results you need to give it enough context but beware of context dilution.
⚠️
WarningIf you install
[superpowers]plugin get ready for it to spin up subagents and burn a ton of tokens in the process. Personally I downloaded their brainstorming, and systematic-debugging skill and removed the plugin.
Consider using MCP servers (like Contact7) if they offer exactly what you need. Some plugins may already cover the same functionality, so try to avoid overlap. Commit them to git so everyone has access.
💡
Pro TipInstall Context7, it is so useful since LLMs do not have always the latest documentations for a library. You can enforce it by adding a
.mcp.json
:
{
"mcpServers": {
"context7": {
"command": "npx",
"args": [
"-y",
"@upstash/context7-mcp@latest"
]
}
}
}
Rather than relying on general knowledge, develop skills tailored to your project. Imagine you are building the backend checkout service for a massive e-commerce platform (think Shopify, Amazon, or a large retail enterprise). This service handles:
Now an engineer on your team asks the coding agent to:
We are releasing a 'Flash Sale' feature tomorrow. We need a new endpoint that accepts a list of product IDs and quantities, bypasses the user's shopping cart, and goes straight to the checkout/payment page. Ensure it validates the flash sale time window."
What happens WITHOUT a skill: the agent reads the prompt, sees "bypass cart" and "straight to payment", and happily writes something like this (LLMs try to follow your instructions as much as possible):
@app.post("/flash-checkout")
def flash_checkout(products, user_id):
total = 0
for p in products:
db.inventory.update_one({"_id": p.id}, {"$inc": {"stock": -p.qty}})
total += p.price * p.qty
total *= 1.08
db.orders.insert_one({"user": user_id, "status": "COMPLETED", "total": total})
stripe.charge(amount=total)
return {"success": True}
Now imagine what would have happened if you had a skill like this in .claude/skills/checkout-orchestration-skill/SKILL.md
:
---
name: checkout-orchestration-skill
description: 'Enforces the distributed saga pattern for the e-commerce checkout pipeline. Use this skill whenever implementing or modifying order placement, payment flows, or cart finalization. It mandates idempotency (Redis), inventory reservation (never direct deduction), strict order state machine transitions (OrderStateMachine), tax aggregation (TaxJar/Vertex routing), async fraud handling (800ms timeout), and Kafka event emission (order_placed topic) for eventual consistency.'
---
## Core Architectural Rules
**NEVER** treat checkout as a single CRUD update. It is a **distributed saga**.
1. **Idempotency First**: Every checkout request must include an `idempotency_key`. If the key exists in Redis, return the cached result immediately.
2. **Reserve, Do Not Deduct**: Never write directly to `inventory` table. Always call `InventoryReservationService.reserve()` which holds stock for 15 minutes (timeout period). Only finalize deduction after `PAYMENT_CONFIRMED` event.
3. **State Machine Only**: The `Order` entity has a strict state machine: `PENDING -> FRAUD_CHECK -> AUTHORIZED -> PAYMENT_CAPTURED -> FULFILLMENT_QUEUE` Never skip states. Never manually set `order.status = 'completed'`. Use `OrderStateMachine.transition('CAPTURE')`.
4. **Tax Agnosticism**: We use TaxJar for US/CA, and Vertex for EU. Always call `TaxAggregator.get_tax()` which handles the routing under the hood. Do NOT hardcode tax rates.
5. **Fraud Timeout**: The fraud check is async. If it takes > 800ms, we proceed with `FRAUD_PENDING` status and place a dead-letter retry, but we do NOT block the user.
6. **Eventual Consistency**: After capturing payment, push exactly one event to the `order_placed` Kafka topic. Must include the `correlation_id` for tracing.
And now if you send the same prompt to the coding agent, it generates real, production-grade, domain-aware code:
@app.post("/flash-checkout")
def flash_checkout(products, user_id, idempotency_key):
cached = redis.get(idempotency_key)
if cached: return cached
if not timezone.now() in flash_sale_window:
raise HTTPException(400, "Flash sale not active")
line_items = [LineItem(p.id, p.qty) for p in products]
reservation_ids = InventoryReservationService.reserve_bulk(line_items)
tax = TaxAggregator.get_tax(line_items, user_id)
order = OrderStateMachine.create(
user_id=user_id,
line_items=line_items,
tax=tax,
status="PENDING"
)
fraud_score = FraudScorer.submit_async(order.id)
auth_token = PaymentGateway.authorize(order.total, user.payment_method)
OrderStateMachine.transition(order.id, "AUTHORIZE", context={"auth_token": auth_token})
event_bus.publish("order_placed", {
"order_id": order.id,
"correlation_id": request.headers["X-Correlation-ID"]
})
response = {"order_id": order.id, "status": "AUTHORIZED"}
redis.setex(idempotency_key, 3600, json.dumps(response))
return response
You can e.g. create a command for labeling GitHub issues:
mkdir -p .claude/commands
cat > .claude/commands/label-github-issue.md << 'EOF'
---
description: Fetch a GitHub issue and apply appropriate labels using gh
argument-hint: <github-issue-url>
allowed-tools: Bash(gh issue view:*), Bash(gh label list:*), Bash(gh issue edit:*), Bash(gh repo view:*)
---
You are labeling a GitHub issue. The issue URL is: $ARGUMENTS
Steps:
1. Parse the URL to extract the owner, repo, and issue number.
2. Run `gh issue view <number> --repo <owner>/<repo> --json title,body,labels` to read the issue's current title, body, and existing labels.
3. Run `gh label list --repo <owner>/<repo>` to see which labels actually exist in this repo. Only use labels from this list, if you strongly believe we lack certain label just let the user know as a side note, do NOT stop here even if you believe certain labels are missing.
4. Based on the issue's title and body, decide which existing labels best apply (e.g. bug, enhancement, documentation, question, good first issue, priority levels, area/* labels, etc).
5. Apply the chosen labels with `gh issue edit <number> --repo <owner>/<repo> --add-label "label1,label2"`.
6. Report back to the user: which labels you applied and a one-line reason for each. Also report back the labels which you believe are good to add with a single line as to why.
If the issue already has labels that are still appropriate, leave them and only add what's missing. If no labels in the repo genuinely fit, say so instead of forcing one on.
EOF
And ensure you are committing them so others will be using the same commands when needed!
AGENTS.md
: Your Agent's Navigation System
The most important investment you can make is in documenting a navigation system for your AI agents. You wanna commit and push this documentation to your VCS. And I believe you already know it but I usually just link to AGENTS.md
in CLAUDE.md
. This way I do not have to copypaste or maintain both.
Also you can have a AGENTS.local.md
/CLAUDE.local.md
for your local setup which is not committed to git. So in AGENTS.md
we usually put stuff such as:
Basically anything you usually need to work on that codebase. But make sure to keep it short and concise since if it is too long then it just take up space in your context window with no real benefits.
❗
NoteYou can have a
AGENTS.md
/CLAUDE.md
in subdirectories of a project, and coding agents load them when reading and working with that directory's files. So there you can be more meticulous with your instructions.I would also like to make it crystal clear that LLMs love to optimize for coverage percentages. They'll often write brittle tests that:
- Overly mock dependencies.
- Test every code path without regard to actual functionality.
- Break when you refactor. But what we want from our test suite cases are:
Good tests: Test the functionality you're building.Refactoring-safe: Shouldn't break when you reimplement. NOTE, we are assuming the APIs remain the same.Logic-catching: Should break when logic breaks.
/context
frequently./clear
and start fresh when needed.PLAN.md
or similar progress log earns its keep: treat it as your project memory and communication log, not just a one-off note.
Hey agent, refactor this 50-person project's entire codebase.
Try to ask LLM:
Break this large task into 10 small, specific steps. Where each step should be independently:
- Specifiable.
- Testable.
- Reviewable by a human.
It is a good practice to start with writing a PLAN.md
to brainstorm what you wanna do. In fact, that's why I downloaded the brainstorming skill from the superpowers plugin. So next time you wanna develop a feature which is big enough for the LLM to derail or misunderstand how it should work, first ask it to brainstorm and write a plan for you. Review and work on that plan, then start with implementing it. So if I wanted to visualize this:
[Explore] => [Plan] => [Confirm] => [Code] => [Commit]
The prompt would be:
Figure out the root cause for issue #983, then propose a few fixes. Let me choose an approach before you code. ultrathink
💡
Pro Tip"ultrathink" is a special "magic keyword" you can add anywhere in your prompt to trigger a maximum reasoning depth mode for that specific request.
If you give your coding agent a way to measure how good it did, the results would be closer to what you wanted since it will iterate over it. For this usually we can:
Remember I have already talked about it here. This is code you stand behind.
Key TakeawayCreate a culture of rejecting mediocre AI-generated code.
It is always a good idea to have another engineer to onboard you. But nowadays you can simply try to utilize LLMs and coding agents to help you with that as well. Ask questions such as:
recoverFromException
take so many arguments? Look through git history to answer.Or you can ask other questions outside of onboarding process:
if/else
in So your coding agent, if it is smart enough, will be able to look at git history whenever needed, and let's imagine you have an MCP server to return your microservice architecture. Then it will use it to gain a deeper understanding of how service A interacts with service B. Sometimes you have to be specific so it knows it must use the MCP server. And there are times it can figure that out itself.
It's becoming increasingly easy to generate tons of code, and the burden is shifting to humans to review it all. Everything above is really one answer to this same problem, combat it with:
Now go back to that first branch you made. Apply what you've read:
AGENTS.md
.Save the results in a separate branch, and compare it against your first attempt. That difference is the whole point of this post.