How I code with LLMs

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub
Recently, like many others, I've been coding with LLMs more frequently. When I mention that I code with LLMs, some people react with surprise and often ask how I utilize them. So I thought I'd write down roughly how I use LLMs in my coding process.
Premise
Naturally, my approach to using LLMs is tailored to the types of work I typically handle. Therefore, it might not be well-suited for other coding scenarios. The work I primarily deal with involves open-source projects where communication with collaborators and users happens mostly asynchronously through written exchanges. Furthermore, I'm mainly involved in library development rather than application development. My primary programming language is TypeScript, which LLMs handle quite well. On the other hand, I work in an ecosystem where existing knowledge becomes outdated relatively quickly, which could be considered a disadvantage in some ways.
In any case, while my LLM utilization approach is tailored to the type of work I typically handle, I've tried to share tips in this article that can be broadly applied to coding in general.
Context is King
When it comes to utilizing LLMs, while there are various considerations such as the model's performance, the most commonly overlooked aspect I've observed is providing sufficient context. People don't realize how much context they rely on when making judgments. From minor memory fragments in our minds to the latest accessible documents and captured images in issues... most of these are typically inaccessible to LLMs on their own. No matter how smart an LLM is, without the necessary context, it will inevitably produce irrelevant results. When I hear people complain that "LLMs are terrible at coding," while some genuinely deal with problems that are difficult for LLMs to solve, in most cases, it's because they failed to provide sufficient context to the LLM.
Perhaps most of the tips I'll cover in this article can be viewed as addressing the question: "How can I effectively provide context to LLMs?"
The Models and Coding Agents I Use
As of September 2025, I use Claude Code for almost all my work. I've been primarily using Claude Code for the past few months, and although I periodically try other tools, I still find Claude Code to be the best fit for me. In fact, I believe Claude Code would be the most suitable for most programmers. This isn't based on rigorous benchmarks but rather my personal experience... for the following reasons:
-
Other models struggle with tool calling. When tools are provided, they should be utilized at the necessary moments. Claude models excel in this aspect. Since tool calling is essential for providing rich context, poor performance in tool calling ultimately reduces the effectiveness of coding with LLMs.
-
Other models' performance deteriorates as the conversation lengthens. At least for me, I don't prefer the "one-shot" approach to coding with LLMs, also known as "vibe coding." Therefore, a model's multi-turn performance is important, and other models tend to forget previous conversations as the dialogue extends.
-
Even among Claude models, Claude Code itself outperforms other LLM coding agents. This is likely due to fine-tuning or system prompts with proprietary techniques. This is why there are unofficial proxies that allow Claude Code to be used with non-Claude models.
Of course, Claude and Claude Code have their drawbacks:
-
They have a relatively short context window, so you need to conserve tokens. While Claude Code's conversation compaction works reasonably well, it can still be stressful. (For instance, conversation compaction happens in English, so in subsequent sessions, it suddenly starts responding in English. Oh, I write my prompts in Korean.)
-
It still lacks LSP support that some LLM coding agents provide. Therefore, you need to separately show type errors or lint errors through command execution. Instead, Claude Code offers hooks, which can provide somewhat similar effects when utilized well.
However, since the advantages outweigh the disadvantages, I'll likely continue using Claude Code as my primary tool unless there are significant changes in the landscape.
Detailed Instructions in Writing
Prompts are better when they're detailed. If your prompt ends in a single sentence, it's likely not a good prompt. Sometimes, you need prompts to create prompts. My approach to prompting is as follows:
First, I write most prompts in GitHub issues. You need to provide sufficient links and should avoid heavily relying on visual information like captured images or diagrams. Of course, since issues are primarily for people, not LLMs, you might not want to include information that's only necessary for LLMs. You don't have to include such information in the issue.[1] If someone else has already created an issue, you can utilize that. If you feel the context in someone else's issue is insufficient, supplement it with comments.
Sometimes, I even write the issues themselves using LLMs. I share relevant documents or situations and ask the LLM to draft the issue. For example, here's the prompt I used to create Issue #11 Plunk transport for adding a Plunk transport to my email sending library, Upyo:
Plunk is an email sending provider. I think it would be good to add Plunk's transport to Upyo. I'd like to create an issue in the issue tracker first. Could you write the issue title and content in English? Instead of a formal write-up that clearly separates problem definition and solution, I'd prefer a more natural tone that sounds like it was written by a person. It doesn't need to be too long either. A paragraph or two should be sufficient.
Reference links:
However, I should note that when I did this, I was using Claude's Projects feature to provide Upyo's existing documentation as RAG context for my prompt.
Next, in Claude Code's plan mode[2], I instruct it as follows:
I need to implement issue https://github.com/dahlia/upyo/issues/11. After examining the issue content and all the related links referenced in it, please create a detailed implementation plan for adding the Plunk transport to the Upyo project.
To be precise, I prefer providing just the issue number instead of the issue link and having it read the issue directly using GitHub MCP. This is because it reads the issue in Markdown format rather than HTML, making it better at following links in the content. If links are particularly important, I sometimes include them again in my Claude Code instructions. I also include any information intended solely for the LLM that I might not have included in the issue.
If you know which source files need to be examined for implementation, providing that information is even better. This helps the LLM avoid unnecessary trial and error when exploring the codebase and uses fewer tokens.
While I used GitHub issues for detailed instructions, I know many people also create document files like PLAN.md for this purpose.
Design by Humans, Implementation by LLMs
My basic principle when using LLMs for coding is to handle the high-level design myself and delegate the detailed implementation to the LLM. When giving instructions, I clearly present the design intent and thoroughly address potential concerns that might lead to mistakes during implementation. In particular, I still often manually create directory or package structures when starting a project. (Though this might be because I never liked cookie-cutter project templates even before the LLM era. Neither templates nor LLMs produce results that satisfy me.)
Since LLMs tend to solve problems using technologies they're most familiar with, it's good to provide explicit instructions for technology choices. Even minor libraries still need human review. When you leave everything to LLMs, they often use outdated versions without security patches. In my case, as I'll discuss later in the AGENTS.md document, I include instructions to check the latest version of a package using the npm view
command before installing libraries.
When it comes to abstraction, I still often design APIs myself. Why? LLM-designed APIs are often subpar. My impression is that LLMs frequently design APIs ad-hoc as they implement. Of course, some people might prefer this approach, in which case they might not have issues with LLM-designed APIs. But in my case, I'm often dissatisfied.
Ultimately, it seems necessary to view LLMs not as all-capable slaves but as colleagues who are smart yet have many immature aspects, and to have humans help in areas where LLMs struggle.
AGENTS.md
Most LLM coding agents provide AGENTS.md or equivalent functionality. For example, Claude Code looks for CLAUDE.md, while Gemini CLI looks for GEMINI.md, but the trend is moving toward standardization with AGENTS.md. I create symbolic links from all vendor-specific files to AGENTS.md and treat AGENTS.md as the canonical version. This allows me to share the same guidelines with collaborators who use different LLM coding agents.
The role of the AGENTS.md document is simple: it serves as guidelines for the project, essentially a system prompt. Most LLM coding agents provide functionality to automatically generate this file, and there's no reason not to use it. You can have it generated automatically and then just correct any errors. More importantly, you need to prevent the AGENTS.md document from becoming outdated over time. The AGENTS.md document should be continuously refined. In particular, you must update the AGENTS.md document after major refactoring. It's good to include this instruction in the AGENTS.md document itself.
So what should you include in the AGENTS.md document? In my case, I include the following:
- The project's goals and overview. Including the repository URL can be surprisingly useful when utilizing tools like GitHub MCP.
- Development tools used by the project. For example, instructions like never using
npm
and only usingpnpm
. - Directory structure and the role of each directory.
- Build and test methods. Especially if the project has unique procedures, they must be described. For example, if code generation must be performed before building or testing, this should be mentioned—of course, the best approach is to automate such procedures with build scripts. This is better for humans and helps conserve tokens.
- Coding or documentation style. It's also good to describe how to use formatters.
- Development methodology. For instance, guidelines like writing regression tests first when fixing bugs, confirming the bug is reproduced by the failing test, and then implementing the fix.
I recommend starting with the above and adding guidelines whenever you notice the LLM making mistakes while using it. For example, I tend to add guidelines to avoid the any
type or as
keyword in TypeScript projects.
Here are the AGENTS.md documents from projects I manage:
Providing Documentation
It's well-known that LLMs have a knowledge cutoff. Unless you're using a brand-new model, it likely has somewhat outdated knowledge about the APIs, CLIs, and other tools you're using. This problem becomes even more significant if you're using non-mainstream programming languages or frameworks.
Therefore, you need to provide knowledge about the programming languages or frameworks you use, and the easiest and most efficient way is to attach Context7 as an MCP. Context7 is a RAG service that maintains relatively up-to-date technical documentation in a vector database and provides relevant document fragments when requested by an LLM. Adding new documentation is also easy, so if the documentation you need isn't registered, you can add it yourself. However, since Context7 might not be utilized separately without specific instructions, you may need to direct the LLM to "check relevant documentation through the Context7 MCP."
When providing technical specification documents like RFCs, it's better to provide links to plain text formats since they're available. In my case, I often need to provide FEP documents when developing fediverse-related projects, and I provide direct links to the Markdown source files rather than HTML-rendered web pages.
Additionally, the practice of providing /llms.txt and /llms-full.txt files on websites is spreading, so utilizing these is also beneficial. (All the software libraries I've created provide /llms.txt and /llms-full.txt files on their project websites.)
However, providing entire technical documents, even in LLM-friendly plain text, can waste tokens, so I recommend using Context7 if available.
Utilizing Plan Mode
Many recent LLM coding agents, including Claude Code, provide a plan mode. This prevents immediate implementation and allows the LLM to create an implementation plan for human review first. In particular, I use "Opus Plan Mode," where I use the expensive Claude Opus 4.1 for planning and the relatively cheaper Claude Sonnet 4 for actual implementation.[3]
I thoroughly review plans and demand revisions if they don't fully satisfy me. Depending on the task, I seem to request at least three or four revisions for any given job. Conversely, without this level of plan refinement, there's a high chance the implementation won't align with my desired direction. LLMs often hold different assumptions than I do, so they might be working toward different goals in various detailed plans. I need to eliminate these discrepancies as much as possible beforehand to align with my intentions.
Let It Run Its Own Feedback Loop
Perhaps the most important aspect of developing with an LLM coding agent is creating conditions where it can adjust its direction on its own. Rather than pointing out each implementation mistake myself, I provide automated tests and static analysis so it can recognize issues and fix implementations on its own.
For example, consider fixing a CSS bug. Having the LLM fix CSS code and then checking the web browser myself is too cumbersome. Instead, it's better to attach Playwright MCP so it can see the screen itself. The key is to enable the LLM to judge whether its work meets the requirements and continue working until those requirements are met.
For similar reasons, instructing it to write test code before implementation is convenient in many ways. You only need to monitor the test code writing process, and then you can pay relatively less attention afterward. In fact, I sometimes write tests myself even when using LLM coding agents. I do this when I feel it would be faster for me to write test code that accurately verifies requirements than to prompt the LLM to do so.
Because I prefer this workflow, I've come to believe that programming languages with more rigorous type systems and stricter lint rules are much more advantageous when utilizing LLM coding agents. I had similar thoughts even before the LLM era.
Occasional Hand Coding
However, due to the many limitations that still exist with LLMs, I still occasionally code by hand. This applies when designing APIs and when I want to write rigorous test code (LLMs tend to write somewhat sloppy test code). And above all, I do the coding that seems fun myself!
I often hear stories of software programmers who became deeply immersed in vibe coding and lost the joy of coding. In my opinion, it's better not to delegate the enjoyable parts to LLMs. This isn't because of the quality of the results, but to maintain motivation as a software programmer. I think a good strategy for coexisting with LLMs is to utilize them as much as possible for the boring and tedious parts—the tasks that make you not want to code. Well, at least this approach seems to work well for me.