Introduction
A couple of months back, I wrote about my first AI coding setup with Cursor and Claude Code Max. Since then, the way I actually work has shifted quite a bit. Cursor moved to the background, Claude Code moved to the front, and I added claude-context for semantic search over the whole codebase plus a couple of MCP servers on top.
In this post, I want to share a concrete example instead of another list of tools: a real pull request where I added a custom Route53 domain pointing to CloudFront in my cdk-vscode-server construct. Real code I maintain, not a toy. I'll show you the exact prompts, where Claude did great on its own, and the spots where I had to grab the wheel.
What has changed
The setup from the last post got me a working Golang template in under two hours, but I still saw flaws in it. What pushed me further was a mix of things: a workshop in Munich, a few conversations afterward, and, honestly, a lot of reading on Reddit, where people share how they run Claude Code day-to-day.
I was meeting Max Ritter and other enthusiasts for an evening workshop. See his post for more details
Two ideas stuck with me. First, let Claude plan before it writes a single line. Second, make it work test-first. That second one clicked after this thread on TDD with Claude Code, and it matches how I already like to work anyway: write the failing test, then make it green.
New setup
So the driver changed. Claude Code Max now does the heavy lifting, and Cursor is only there for autocompletion. On top, I run a small set of MCP servers:
- context7 to keep the model current on library APIs
- the AWS MCP server for CDK and AWS specifics
- claude-context for semantic code search, so Claude finds the relevant construct without reading half the repo first
Personally, I don't want a huge toolbox. These three cover the gaps I actually hit: fresh docs, AWS knowledge, and finding my way around a bigger codebase.
Walkthrough
Two commands do most of the steering. /mcp shows me which MCP servers are connected, so I can confirm context7, the AWS server, and claude-context are all live before I start.
For the model and the mode, I use /model to pick between Opus and Sonnet, and SHIFT+TAB to cycle the modes until I land on plan mode. Plan mode is the important one here: Claude reasons through the change and writes a plan without touching a single file yet.
Auto mode
My flow is plan first, then let it run. I start in plan mode on Opus and ask the open question:
How would you add custom Route53 domain pointing to cloudfront
I read the plan, and only when I'm happy with it do I hand it back for implementation, this time asking for test-driven development:
Use this plan to implement it with TDD (unit tests)
What came back was a clean red-green plan, tests first, implementation after:
- Write failing unit tests for custom domain properties (TDD Red phase)
- Write failing tests for Route53 record creation
- Write failing tests for CloudFront custom domain configuration
- Write failing tests for ACM certificate handling (existing + auto-create)
- Write failing tests for hosted zone lookup and validation
- Write failing tests for custom domain output and backward compatibility
- Extend VSCodeServerProps interface with new domain properties
- Add required CDK imports for Route53, ACM, and related services
- Implement domain validation and configuration logic
- Implement ACM certificate handling (existing vs auto-create)
- Implement Route53 hosted zone lookup and A record creation
- Update CloudFront distribution with custom domain configuration
- Update domain name output to use custom domain when provided
- Add CDK-nag suppressions for new resources
- Run tests to ensure all pass and no regressions
- Create example implementation with custom domain
The first six steps are all failing tests. That is exactly the order I would have written it in myself.
And then it worked through the list until the tests went green:
Manual intervention
This is the part people leave out of the "look how fast AI is" posts. Left completely alone, this change would have broken later.
The first one was the nginx config. The installer sets an nginx server_name for the CloudFront domain, and for a custom domain, which would have failed during the integration tests. So I pointed Claude straight at it:
In installer.ts, there is a config for nginx with a servername for CloudFront. Fix it so it also works with the provided domain.
Then the integration tests themselves. These are expensive: they create real resources and take time, so I add them at the very end, and I don't let them run automatically:
Add an integ test for the custom domain (do not run it). Assume that the testdomain is 'vscode-server-test.mavogel.xyz' with the hostedZoneId 'Z123456789EXAMPLE
From my rootmail project I already knew that lookup functions don't work in integ-tests, so I kept an eye on that one.
At this point, the whole custom-domain feature was done in 26 minutes.
Now the integ-tests. Running the dry-run surfaced an error:
Fix the error when running
npm run integ-test -- --dry-run
And here is where I had to be firm. Claude wanted to simply remove the NodeTagger to make the error go away. That is the classic move: delete the inconvenient thing. I didn't want that. The tagging is there on purpose:
Keep the NodeTagger and fix the issue with the priority
The final version keeps the aspect and is careful about which constructs it tags, which avoids the infinite loop that caused the error in the first place:
/**
* Tags all the resources in the construct
*/
class NodeTagger implements IAspect {
private readonly tags: { [key: string]: string };
constructor(tags: { [key: string]: string }) {
this.tags = tags;
}
visit(node: IConstruct) {
// Only tag L1 constructs (CfnResource) and L2 constructs that represent AWS resources
// This prevents infinite loops by avoiding tagging of intermediate constructs
const nodeType = node.constructor.name;
// Check if this is a CDK L1 construct (starts with 'Cfn') or known taggable L2 constructs
const isTaggableConstruct =
nodeType.startsWith('Cfn') ||
nodeType.includes('Instance') ||
nodeType.includes('Vpc') ||
nodeType.includes('Subnet') ||
nodeType.includes('SecurityGroup') ||
nodeType.includes('Volume') ||
nodeType.includes('Distribution') ||
nodeType.includes('LoadBalancer') ||
nodeType.includes('TargetGroup');
// Skip constructs that are known to cause issues
const isProblematicConstruct =
nodeType.includes('Certificate') ||
nodeType.includes('HostedZone') ||
nodeType.includes('CustomResource') ||
nodeType.includes('Provider') ||
nodeType.includes('Function') ||
nodeType.includes('Role') ||
nodeType.includes('Policy');
if (isTaggableConstruct && !isProblematicConstruct) {
Object.entries(this.tags).forEach(([key, value]) => {
try {
Tags.of(node).add(key, value);
} catch (error) {
// Silently ignore tagging errors
}
});
}
}
}If there is one thing that still annoys me in all of this, it is CDK integration tests. They are slow, and they cost real money, and they are exactly the part where the AI still needs the most supervision.
Conclusion
Personally, I'm convinced the skills that matter now are prompt and context engineering, not typing every line yourself. Give the model a plan you actually reviewed, the right context via MCP servers, and a guardrail like TDD, then check every step, as you would review a junior's pull request. The 26 minutes only happened because I knew what I wanted and stepped in the second it drifted.
You can see the full result in the summary PR: MV-Consulting/cdk-vscode-server#63.
I hope this gives you a realistic picture of a plan-first, test-driven session, the good parts and the moments where you still have to take over yourself. Happy prompting!



