This is the third time I looked for the clever part of a coding agent, and the third time I found a text file instead. I now think that this is the important part of the story.
The first time I opened the loop, and it was fourteen lines of while. The second time I looked for the part that stops the agent from doing damage, and I found a second model in a second loop. This time the subject is skills, and I must be careful here, because skills are not new or unusual. Most people know what a skill is, and many people have written one, but I had never looked at what the software does when it loads one.
The same instructions, every time
The problem is ordinary and completely real, because I typed the same paragraph again and again. “Write the commit message in this form.” “Review this change against that list.” I did this several times each day, and I wrote it a little differently every time.
There were two places to keep that text, and I did not like either of them. The system prompt is a constant in Main, so everything in it goes to the model on every turn. This happens even when the task has no relation to it. The other option is to type the text again. A skill is the third option: a file that you load when the task needs it. I did not invent any part of this.
What I thought I must build
Here is the honest part. I used skills for months, but I never asked what happens when one loads. I knew the file format, and I could also explain progressive disclosure to you. The main file loads and the reference files do not. The model reads a reference file only when the task needs it. It is a good idea, and I was sure that there was a mechanism below it.
I thought that the loader was the interesting part, and that it would take me a weekend to write. This is the loader.
conversation.add(new UserMessage(load(skill, skills.body(skill))));conversation.add(new AssistantMessage(loaded(skill), List.of()));
konacode reads the file and adds two messages to the conversation. There is no retrieval in it. There is no index, no embedding, no similarity score, and nothing that selects the correct skill for the task. There is not even a search. The file goes into the conversation complete, because I typed its name.
The command also makes no call to the model, so /skill returns immediately and cannot fail on the network. This is not good design on my part, because there is simply nothing for it to ask.
And the part that loads only when it is necessary?
This is the best thing that I learned this month. Reference files are the part that must load only when the model needs them. This is the sentence that konacode adds to make that work.
Use read_file to read a file inside that folder.
It is one sentence, and it sits in the message that carries the skill. The model receives this text.
The skill `commit-message` is loaded. Its folder is`/home/b/.konacode/skills/commit-message`. Use read_file to read a file insidethat folder.<the body of SKILL.md>
There is nothing more than that. konacode has no loader for reference files, no second code path, and no additional class. The skill text mentions reference.md, and the model decides that it needs that file. It then calls a tool that was already there to read files. I get this result again and again, and it is always the same. The mechanism that I look for is a convention, and the convention operates because the model does the reading.
The format has three levels, and konacode uses two of them. The first level is the name and the description of every skill, and it stays in the context so that the model can select a skill without help. The second level is the body of SKILL.md, and the third level is the reference files. konacode loads the body complete, and it reads a reference file only when the model asks for it. But konacode gives the model no first level at all, so the model does not know which skills exist, and it does not know that any skill exists.
Who selects the skill
The next decision goes in the other direction, and I am ready to argue about this one. In the last post I said several times that my code decides nothing, because the loop offers the tools and the model selects. Here I took a decision back on purpose. The model cannot load a skill. There is no load_skill tool, and /skill is a command that a person types.
I examined the alternative with care. A load_skill tool must list the name and the description of every skill in its own description. The model needs that list before it can select one, and a tool description goes to the model on every turn. So each new skill makes every request larger for ever, and this includes all the requests about other subjects. The command costs nothing until the moment when you use it.
There is a real cost on my side too, and I will not hide it. The model does not know that a skill exists, so it cannot select the correct one without help. That was much of the promise, and I can change my mind later. The design document holds the alternative and its cost, which is how I usually record that I can be wrong 🙂
konacode must answer itself
Here is one more detail, and it is the kind that you find only when you build something. Look at those two lines again, and ask why konacode adds a second message that it wrote itself.
The reason is that a conversation is a data structure, and a data structure has rules. /skill adds a user message and then stops, and nothing answers that message. The next line that you type adds a second user message, so the history holds two user messages together. The Anthropic Messages API refuses a history in that shape.
So konacode writes the assistant message itself: “The skill commit-message is loaded.” It speaks as konacode, and it never speaks as the model. Two other parts of the code already do this for the same reason.
The next decision comes from the same structure. The skill text goes into a user message and not into the system prompt, because /clear restarts the conversation with the system message alone. A skill in the system prompt stays after /clear, and I wanted /clear to remove it. The position of the text is what gives me both behaviours.
The format is not mine
~/.konacode/skills/ reads SKILL.md in the Claude Code format, with the same header. This is deliberate, and it is not exciting: a format that nobody else writes gives the user an empty folder. There are skills on my machine already, and they operate in konacode because I invented nothing.
It has a cost. The header is YAML, but konacode has one Jackson module and no YAML parser. A new dependency is a decision that I did not want to make. So FrontMatter reads two keys with single-line values, and it ignores the remainder. It is not a YAML parser, and the class says this clearly, because the next reader will think that it is one.
What it cost
The test count went from 226 to 280, and all the tests operate offline. But the two defects that the reviews found are more interesting than the count.
The first defect starts in lookup, which throws an exception when the name is not a folder name. Repl caught nothing, and Main catches Exception and stops the program. So the command /skill .. ended the session. A spelling mistake in a skill name removed the full conversation. That is embarrassing, but it was not difficult to correct.
The second defect has the same shape as the last post, in the opposite direction. My text to the model contained this sentence: “A path in the text below is relative to that folder.” It looks like documentation, but it is not true. konacode does not read the skill text, and it does not change a path inside it. A skill that names a file in your project means the file in your project. I told the model something about my software that my software does not do. A test now compares the complete sentence, and not three parts of it. The three parts let the false sentence return with a green test suite.
Last time the lesson was that the model can give me false information. This time I gave the model false information, and I did not even do it on purpose.
Where is the code?
https://github.com/bbossola/konacode
/skill is pull request #27, and the design document is in docs/ with it. The document also holds the alternative that I rejected, and what it would have cost me.
Three posts into this work, my summary is that the difficulty is almost never in the place where the words tell you to look. It is not in the planner, it is not in the safety engine, and it is not in the retrieval. It is in the decision about who selects, and in the rules that a conversation obeys. It is also in careful attention to what you tell the model is true.

