<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>HAHWUL</title>
    <link>https://www.hahwul.com/</link>
    <description>Security Engineer, Developer and H4cker.</description>
    <atom:link href="https://www.hahwul.com/rss.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>I&apos;ve joined the Kemal Core Team!</title>
      <link>https://www.hahwul.com/posts/2026/i-ve-joined-the-kemal-core-team/</link>
      <guid>https://www.hahwul.com/posts/2026/i-ve-joined-the-kemal-core-team/</guid>
      <description>A Security Report, and What Came After</description>
      <content:encoded><![CDATA[<p>Earlier this month, I joined the Core Team of <a href="https://kemalcr.com">Kemal</a>, Crystal’s leading web framework.</p>
<p>It started with reporting and fixing <a href="https://github.com/kemalcr/kemal/security/advisories/GHSA-gvhf-4v24-f8r5">request smuggling via WebSocket upgrades</a>, which led to an invitation to join the Core Team. Since joining, I've spent my time reviewing and fixing a number of security issues leading up to the <a href="https://github.com/kemalcr/kemal/releases/tag/v1.13.0">v1.13</a> release.</p>
<p>Anyway, a bit late, but I wanted to share it here on the blog as well :)</p>
<p><img loading="lazy" src="https://www.hahwul.com/posts/2026/i-ve-joined-the-kemal-core-team/images/1.webp" alt="https://x.com/crystalkemal/status/2086725990114369996" /></p>
]]></content:encoded>
      <pubDate>Wed, 26 Aug 2026 00:00:00 +0000</pubDate>
      <category>crystal</category>
      <category>kemal</category>
    </item>
    <item>
      <title>Rust and Crystal: My Two Main Languages</title>
      <link>https://www.hahwul.com/posts/2026/rust-and-crystal/</link>
      <guid>https://www.hahwul.com/posts/2026/rust-and-crystal/</guid>
      <description>Balancing Popularity and Quiet Power</description>
      <content:encoded><![CDATA[<p>Before I get into the post, here's a funny habit of mine: I tend to use two different things side by side for the same purpose.<br />
macOS and Linux, Helix and Zed, Claude and Grok, and my left and right hands come to mind right away. (I'm ambidextrous, by the way.)</p>
<p>Programming languages are no different. I started with C/C++, went through a bunch of languages along the way, and these days I've settled on Rust and Crystal. I think this pair is a really attractive combination. So today I want to talk a bit about why I find these two so appealing together, and what I'm building with them.</p>
<h2 id="rust-and-crystal">Rust and Crystal</h2>
<p>Both are compiled, typed languages, and both are very fast. When it comes to popularity, though, they've gone in very different directions. Rust started with a strong fan base and is now a thoroughly mainstream language. Crystal isn't that popular even within its own community. People sometimes call it a quiet village, but the users it does have are passionate.</p>
<p>There are a few reasons I picked these two as my main languages, but the core ones are simple: Rust's stability and popularity, and Crystal's syntax plus my personal attachment to it.</p>
<h2 id="productivity-and-ecosystem">Productivity and Ecosystem</h2>
<p>A language's popularity says a lot about its vitality and the strength of its ecosystem. That translates into productivity, and it helps a program stay maintained for a long time. Rust is already an excellent language in terms of what it did for stability and speed, but the solid ecosystem and popularity on top of that make it even stronger.</p>
<p><img loading="lazy" src="https://www.hahwul.com/posts/2026/rust-and-crystal/images/crates.webp" alt="" /></p>
<p>Seen that way, a popular language is being developed by the combined effort of many people, whether we notice it or not. For me, Rust is exactly what fills that spot. For stable collaboration, I really have no choice but to go with a popular language.</p>
<h2 id="love-and-learning">Love and Learning</h2>
<p>So you might wonder why Crystal, which isn't popular, is one of my main languages. There are practical reasons, like syntax that feels familiar from Ruby (which I used a lot before), fast performance, and lightweight binaries. But at the bottom of it is personal attachment. I just like the language.</p>
<pre><code class="language-ruby hljs"><span class="hljs-comment"># Crystal code is pretty intuitive, right? :)</span>
channel <span class="hljs-operator">=</span> <span class="hljs-variable constant_">Channel</span><span class="hljs-punctuation">(</span><span class="hljs-variable constant_">Int32</span><span class="hljs-punctuation">)</span><span class="hljs-operator">.</span>new

<span class="hljs-number">3</span><span class="hljs-operator">.</span>times <span class="hljs-keyword">do</span> <span class="hljs-operator">|</span>i<span class="hljs-operator">|</span>
  spawn <span class="hljs-keyword">do</span>
    channel<span class="hljs-operator">.</span>send <span class="hljs-number">10</span> <span class="hljs-operator">*</span> <span class="hljs-punctuation">(</span>i <span class="hljs-operator">+</span> <span class="hljs-number">1</span><span class="hljs-punctuation">)</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>

<span class="hljs-built_in">puts</span> channel<span class="hljs-operator">.</span>receive
</code></pre>
<p>In the end, it's people who read, write, and use a language. To keep going on long-running projects, you need some attachment to the language. That's true of more than just languages, of course. It's what lets you keep doing the work you enjoy. In that sense, Crystal is a language I've put a lot of attachment into, and one I enjoy using. I think it's one I can keep using steadily as the years go by.</p>
<p>There's one more big reason. A weaker ecosystem actually helps a lot when it comes to learning. Since there are no libraries for niche features, I end up building them myself and understanding how they work along the way. I learned a lot from that.</p>
<h2 id="balancing-popularity-and-quiet-power">Balancing Popularity and Quiet Power</h2>
<p>So what I'm trying to say with this post is that when you're choosing a programming language, you don't have to stick to the popular ones.</p>
<p>Sometimes it's just as valuable to pick up a language that's still growing, join its community, and learn along the way. If you have the time, try using a popular language and a growing one side by side. You'll get more out of it than you'd expect :)</p>
<p><img loading="lazy" src="https://www.hahwul.com/posts/2026/rust-and-crystal/images/power.webp" alt="" /></p>
]]></content:encoded>
      <pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate>
      <category>rust</category>
      <category>crystal</category>
    </item>
    <item>
      <title>Traveling with Hermes in Japan</title>
      <link>https://www.hahwul.com/posts/2026/traveling-with-hermes-in-japan/</link>
      <guid>https://www.hahwul.com/posts/2026/traveling-with-hermes-in-japan/</guid>
      <description>My Remote Setup and Project Workflow While Traveling</description>
      <content:encoded><![CDATA[<p>I spent the past week traveling in Japan. It's a country I visit every year, so it's familiar territory, but this year I prepared a little experiment. I set up the Hermes Agent on the Mac Studio at home, and kept directing it through Discord to work on projects while I was away. The results turned out better than I expected, and I wanted to share that experience.</p>
<p>In my AI workflows I sometimes hand the orchestrator role to the AI, but for important projects I prefer to stay hands-on. This time, driving Hermes remotely let me make good use of travel and downtime. Of course, I brought my MacBook too, so I still did some work directly late at night or early in the morning.</p>
<h2 id="hermes-agent">Hermes Agent</h2>
<p><a href="https://hermes-agent.nousresearch.com">Hermes</a> is an open-source AI Agent from Nous Research, and it's been getting attention as an alternative to OpenClaw. Its core feature is self-improvement. After the Agent finishes a task, it reviews the result itself, remembers patterns, or turns them into new skills it can reuse. The more you use it, the smarter it gets.</p>
<p>I've personally been a fan of the self-learning concept, so I'd been testing it locally since March. This time I finally put it to work on an actual open-source project.</p>
<p><img loading="lazy" src="https://www.hahwul.com/posts/2026/traveling-with-hermes-in-japan/images/hermes.webp" alt="Hermes Agent" /></p>
<h3 id="setup">Setup</h3>
<p>Hermes supports a variety of providers, so you can configure it however you like. Since I use Claude, Codex, and Gemini subscriptions heavily, I hooked up GitHub Copilot, which had more room to spare, for lighter tasks. (I'd never managed to use up Copilot's monthly quota anyway, so this worked out nicely.)</p>
<p>Since provider handling is a lightweight task, I mostly used the Sonnet 4.6 model. If you want to save even more on cost, Grok Code Fast would also be a solid choice.</p>
<p><img loading="lazy" src="https://www.hahwul.com/posts/2026/traveling-with-hermes-in-japan/images/copilot.webp" alt="Output of hermes model" /></p>
<p>For the messaging gateway, I went with <a href="https://hermes-agent.nousresearch.com/docs/user-guide/messaging/discord">Discord</a>. You create a Discord bot and register its token with Hermes.</p>
<p>Personally, I think the messaging channel is critical from a security perspective. Discord lets you control bot permissions in fine detail, and Hermes' settings let you restrict access to specific user IDs via <code>ALLOWED_USERS</code>. With this setup, the bot only carries the permissions it actually needs, and random users can't just invoke it at will.</p>
<p>If you look at <code>~/.hermes/.env</code>, it's defined like this:</p>
<pre><code class="language-toml hljs">DISCORD_BOT_TOKEN<span class="hljs-punctuation">=</span>********
DISCORD_ALLOWED_USERS<span class="hljs-punctuation">=</span>********
DISCORD_HOME_CHANNEL<span class="hljs-punctuation">=</span>********
</code></pre>
<h3 id="workflow">Workflow</h3>
<p>Coding requires precision, so Hermes (Copilot) alone isn't quite enough in terms of quality. So most of the real work got delegated to Claude Code, Codex, and Gemini.
Early on I had to give multiple directions, but once the conversation built up some context, Hermes started routing to the appropriate model (Claude, Codex, Gemini) on its own depending on the situation, which was pretty appealing. Copilot was mostly used for the chat channel and light tasks.</p>
<div class="mermaid">flowchart LR
    A[Me] --&gt;|Send Message| B(Hermes Agent with Github Copilot)
    B --&gt; C{Thinking}
    C --&gt;|Write Code| D[Claude Code]
    C --&gt;|Code Refactoring| E[Codex]
    C --&gt;|Design Task| F[Gemini]
</div>
<h3 id="block-macos-sleep">Block macOS sleep</h3>
<p>Leaving the Mac Studio idle for long periods puts it to sleep automatically, so I needed to prevent that. I handled it with a small app I've been developing myself called <a href="https://apps.apple.com/kr/app/nodecaf/id6762029386?l=en-GB&amp;mt=12">NoDecaf</a>, which was nice because it doubled as a real-world test.</p>
<h2 id="in-japan">In Japan</h2>
<p>Before leaving, I finished plenty of testing and dropped the main work instructions into Discord, then went off to enjoy the trip. Feedback requests and permission approval notifications came in occasionally, but nothing overwhelming. I could check and handle them pretty casually.</p>
<div class="images-full-width">


<div class="images-grid">
    
    <div class="images-grid-item">
        <img src="https://www.hahwul.com/posts/2026/traveling-with-hermes-in-japan/images/1.webp" alt="" loading="lazy">
    </div>
    
    <div class="images-grid-item">
        <img src="https://www.hahwul.com/posts/2026/traveling-with-hermes-in-japan/images/2.webp" alt="" loading="lazy">
    </div>
    
</div>
</div>
<p>Once the conversation had built up some history, I could keep my instructions brief and Hermes would still handle things well, which was satisfying. Watching it notice a usage limit and queue up a pending task on its own honestly moved me a little :D</p>
<h2 id="generated-skill">Generated Skill</h2>
<p>I could see the skills Hermes had automatically generated from its workflow. The <code>hwaro-examples-batch</code> skill visible in the image above is a good example. It studied the patterns of tasks I frequently assign around <a href="https://github.com/hahwul/hwaro-examples">hahwul/hwaro-examples</a> and turned them into a skill on its own.</p>
<p>Skills are stored under <code>~/.hermes/skills</code>, with built-in skills and user-generated ones managed together. <code>hwaro-examples-batch</code> lived at <code>~/.hermes/skills/github/hwaro-examples-batch</code>, and looking at its SKILL.md, it documented the trigger phrases I usually use, the local clone path (interestingly, even though I told it a specific path was fine to use, it set up a separate one, probably to avoid conflicts with the user's own workspace), and the scripts needed to carry out the task.</p>
<p><img loading="lazy" src="https://www.hahwul.com/posts/2026/traveling-with-hermes-in-japan/images/hwaro-example.webp" alt="Screenshot of SKILL" /></p>
<p>Since skills are built from actual performed work, it's starting to feel like generating them automatically from the AGENT's workflow might be a better choice than writing SKILLs by hand. I'll have to keep running Hermes and squeeze out more skills.</p>
<h2 id="jules">Jules</h2>
<p>Separately from Hermes, <a href="https://jules.google">Jules</a> was actually still running automatically during the trip too. I keep Jules on lighter tasks, and because it's purely cloud-based, I can run it comfortably from home or on the road. A few scheduled jobs are set up on the hwaro-example side, periodically identifying pages with problems, fixing them, and sending PRs. The PRs it opens then get handled by Hermes using Codex.</p>
<p><img loading="lazy" src="https://www.hahwul.com/posts/2026/traveling-with-hermes-in-japan/images/jules.webp" alt="Jules" /></p>
<p>If you set up the working environment within Jules' Environment, there will be little difference from running it on an actual local PC.</p>
<h2 id="areas-for-improvement">Areas for Improvement</h2>
<p>Convenience comes with security risks. One thing I felt going through this flow is that permission separation matters. In particular, for high-privilege accounts like GitHub, I think it's worth creating a separate dedicated account just for the Agent.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Since I usually do most of my work sitting right at my Mac, my biggest question was whether I could actually use AI effectively from just my phone while traveling. It turned out to be more comfortable than I expected, and the setup seems to save a lot of time during transit, so once vacation is over I might try applying this flow to my commute as well. Serious work is still better done with me at the keyboard (it's easier for me and for the AI, and the results are better), but anything that can be verified through skills (e.g., development with tight test coverage, tasks that don't need broad permissions) seems better handled through Hermes in the gaps.</p>
<p>Being able to fully enjoy my own time while small tasks keep ticking along is really appealing. Raising an Agent with self-improvement like Hermes also feels like a pretty meaningful experience. If it sounds interesting, I'd recommend giving it a try.</p>
]]></content:encoded>
      <pubDate>Sun, 19 Apr 2026 00:00:00 +0000</pubDate>
      <category>ai</category>
      <category>hermes</category>
    </item>
    <item>
      <title>Building AI-Friendly CLIs</title>
      <link>https://www.hahwul.com/posts/2026/building-ai-friendly-clis/</link>
      <guid>https://www.hahwul.com/posts/2026/building-ai-friendly-clis/</guid>
      <description>JSON-First Design with Schema Commands</description>
      <content:encoded><![CDATA[<p>These days, AI agents are writing code, calling tools, and even handling deployments. With that shift, the CLI is getting attention again. GUIs and web dashboards are great for humans, but from an AI agent's perspective, CLIs are a much easier interface to work with.</p>
<p>But most existing CLIs were designed for humans. Pretty table output, color codes, shorthand flags are all nice for human eyes but pretty painful for an agent to parse. Output formats also shift subtly between versions, and figuring out the exact usage often means going off to read the docs.</p>
<p>This week, I did a major overhaul of our team's internal CLI at work, rebuilding it around JSON I/O and schema commands. The agent's task success rate jumped noticeably. Based on that experience, I want to share some thoughts on how to build AI-friendly CLIs.</p>
<h2 id="why-json-first-input-output-matters-for-ai-agents">Why JSON-First Input / Output Matters for AI Agents</h2>
<h3 id="human-vs-ai-cli-usage-patterns">Human vs AI CLI Usage Patterns</h3>
<p>When humans use a CLI, they check <code>--help</code>, read man pages, eyeball error messages, and iterate through trial and error. Even if the output changes a bit, they adapt by reading the context. AI agents, on the other hand, take the output as a raw string and process it literally. They have to parse table-formatted output with regex, and the moment column order shifts or line breaks change, things break immediately.</p>
<pre><code class="language-bash hljs"><span class="hljs-comment"># This is convenient for humans, but...</span>
$ kubectl get pods
NAME                     READY   STATUS    RESTARTS   AGE
my-app-7d4b8c6f5-x2k9z  1/1     Running   <span class="hljs-number">0</span>          3d

<span class="hljs-comment"># This is what agents need</span>
$ kubectl get pods -o json
<span class="hljs-operator">{</span>
  <span class="hljs-string">&quot;items&quot;</span>: <span class="hljs-operator">[{</span>
    <span class="hljs-string">&quot;metadata&quot;</span>: <span class="hljs-operator">{</span><span class="hljs-string">&quot;name&quot;</span>: <span class="hljs-string">&quot;my-app-7d4b8c6f5-x2k9z&quot;</span><span class="hljs-operator">}</span>,
    <span class="hljs-string">&quot;status&quot;</span>: <span class="hljs-operator">{</span><span class="hljs-string">&quot;phase&quot;</span>: <span class="hljs-string">&quot;Running&quot;</span>, <span class="hljs-string">&quot;containerStatuses&quot;</span>: <span class="hljs-operator">[{</span><span class="hljs-string">&quot;ready&quot;</span>: true<span class="hljs-operator">}]}</span>
  <span class="hljs-operator">}]</span>
<span class="hljs-operator">}</span>
</code></pre>
<h3 id="the-pain-of-unstructured-text-output">The Pain of Unstructured Text Output</h3>
<p>Unstructured text output causes more problems for agents than expected. Here are some patterns I actually ran into:</p>
<ul>
<li>Inconsistent parsing: sometimes the output has headers, sometimes it doesn't</li>
<li>Locale dependency: date/number formats change based on system locale</li>
<li>Color code pollution: ANSI escape codes sneak in and break string comparisons</li>
<li>Progress bar collisions: stderr and stdout get mixed up, garbling the output</li>
<li>Silent truncation: long values get clipped to <code>...</code> with no way to detect it</li>
</ul>
<p>Once you start handling these one by one, your agent code ends up buried in CLI parsing logic. You spend more time interpreting output than on the actual work.</p>
<h3 id="advantages-of-json">Advantages of JSON</h3>
<p>JSON I/O solves most of these.</p>
<ul>
<li>Type safety: numbers are numbers, strings are strings. You can distinguish <code>&quot;3&quot;</code> from <code>3</code></li>
<li>Schema-based validation: define and validate input/output shapes upfront with JSON Schema</li>
<li>Easy chaining: instantly parseable by <code>jq</code>, pipelines, and any programming language</li>
<li>Consistency: identical output regardless of locale or terminal settings</li>
<li>Structured errors: return errors as JSON so agents can identify error types and respond appropriately</li>
</ul>
<pre><code class="language-json hljs"><span class="hljs-punctuation">{</span>
  <span class="hljs-name">&quot;error&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-name">&quot;code&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;RESOURCE_NOT_FOUND&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-name">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Pod &#39;my-app&#39; not found in namespace &#39;default&#39;&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-name">&quot;suggestions&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;Check namespace with --namespace flag&quot;</span><span class="hljs-punctuation">]</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<h3 id="real-world-test-results-from-our-project">Real-World Test Results from Our Project</h3>
<p>Here's a quick summary of what changed after adding a <code>--json</code> flag to the CLI and having agents perform the same tasks:</p>
<table>
<thead>
<tr>
<th>Metric</th>
<th>Text Output</th>
<th>JSON Output</th>
</tr>
</thead>
<tbody>
<tr>
<td>Task success rate</td>
<td>Around 60%</td>
<td>Around 90%</td>
</tr>
<tr>
<td>Average retries</td>
<td>2.3 times</td>
<td>0.4 times</td>
</tr>
<tr>
<td>Parsing-related errors</td>
<td>41% of all errors</td>
<td>Nearly 0%</td>
</tr>
</tbody>
</table>
<p>These numbers are from the specific tasks I tested, so your mileage may vary. Still, it was surprising that just switching to JSON made this much difference.</p>
<h2 id="the-schema-command-letting-ai-learn-and-adapt-at-runtime">The Schema Command: Letting AI Learn and Adapt at Runtime</h2>
<p>JSON I/O alone is a huge improvement, but there's a way to take it one step further: the schema command.</p>
<h3 id="inspiration-from-google-workspace-cli-gws">Inspiration from Google Workspace CLI (gws)</h3>
<p>This idea was inspired by <a href="https://github.com/googleworkspace/cli">Google Workspace CLI (gws)</a>. gws has a structure that lets you query schema information per resource at runtime. Looking at that, I thought: &quot;Why not let the agent ask the CLI directly instead of reading documentation?&quot;</p>
<h3 id="how-the-schema-subcommand-works">How the Schema Subcommand Works</h3>
<p>The concept is simple. Add a <code>schema</code> subcommand to your CLI: specify a resource and action, and it returns the JSON Schema for that command's input and output.</p>
<pre><code class="language-bash hljs">$ mytool schema user.create
<span class="hljs-operator">{</span>
  <span class="hljs-string">&quot;</span><span class="hljs-variable">$schema</span><span class="hljs-string">&quot;</span>: <span class="hljs-string">&quot;http://json-schema.org/draft-07/schema#&quot;</span>,
  <span class="hljs-string">&quot;description&quot;</span>: <span class="hljs-string">&quot;Create a new user&quot;</span>,
  <span class="hljs-string">&quot;input&quot;</span>: <span class="hljs-operator">{</span>
    <span class="hljs-string">&quot;type&quot;</span>: <span class="hljs-string">&quot;object&quot;</span>,
    <span class="hljs-string">&quot;required&quot;</span>: <span class="hljs-operator">[</span><span class="hljs-string">&quot;email&quot;</span>, <span class="hljs-string">&quot;role&quot;</span><span class="hljs-operator">]</span>,
    <span class="hljs-string">&quot;properties&quot;</span>: <span class="hljs-operator">{</span>
      <span class="hljs-string">&quot;email&quot;</span>: <span class="hljs-operator">{</span><span class="hljs-string">&quot;type&quot;</span>: <span class="hljs-string">&quot;string&quot;</span>, <span class="hljs-string">&quot;format&quot;</span>: <span class="hljs-string">&quot;email&quot;</span><span class="hljs-operator">}</span>,
      <span class="hljs-string">&quot;role&quot;</span>: <span class="hljs-operator">{</span><span class="hljs-string">&quot;type&quot;</span>: <span class="hljs-string">&quot;string&quot;</span>, <span class="hljs-string">&quot;enum&quot;</span>: <span class="hljs-operator">[</span><span class="hljs-string">&quot;admin&quot;</span>, <span class="hljs-string">&quot;member&quot;</span>, <span class="hljs-string">&quot;viewer&quot;</span><span class="hljs-operator">]}</span>,
      <span class="hljs-string">&quot;name&quot;</span>: <span class="hljs-operator">{</span><span class="hljs-string">&quot;type&quot;</span>: <span class="hljs-string">&quot;string&quot;</span>, <span class="hljs-string">&quot;maxLength&quot;</span>: 100<span class="hljs-operator">}</span>
    <span class="hljs-operator">}</span>
  <span class="hljs-operator">}</span>,
  <span class="hljs-string">&quot;output&quot;</span>: <span class="hljs-operator">{</span>
    <span class="hljs-string">&quot;type&quot;</span>: <span class="hljs-string">&quot;object&quot;</span>,
    <span class="hljs-string">&quot;properties&quot;</span>: <span class="hljs-operator">{</span>
      <span class="hljs-string">&quot;id&quot;</span>: <span class="hljs-operator">{</span><span class="hljs-string">&quot;type&quot;</span>: <span class="hljs-string">&quot;string&quot;</span>, <span class="hljs-string">&quot;format&quot;</span>: <span class="hljs-string">&quot;uuid&quot;</span><span class="hljs-operator">}</span>,
      <span class="hljs-string">&quot;email&quot;</span>: <span class="hljs-operator">{</span><span class="hljs-string">&quot;type&quot;</span>: <span class="hljs-string">&quot;string&quot;</span><span class="hljs-operator">}</span>,
      <span class="hljs-string">&quot;role&quot;</span>: <span class="hljs-operator">{</span><span class="hljs-string">&quot;type&quot;</span>: <span class="hljs-string">&quot;string&quot;</span><span class="hljs-operator">}</span>,
      <span class="hljs-string">&quot;created_at&quot;</span>: <span class="hljs-operator">{</span><span class="hljs-string">&quot;type&quot;</span>: <span class="hljs-string">&quot;string&quot;</span>, <span class="hljs-string">&quot;format&quot;</span>: <span class="hljs-string">&quot;date-time&quot;</span><span class="hljs-operator">}</span>
    <span class="hljs-operator">}</span>
  <span class="hljs-operator">}</span>
<span class="hljs-operator">}</span>
</code></pre>
<p>Even better if you can also query the list of available resources:</p>
<pre><code class="language-bash hljs">$ mytool schema --list
<span class="hljs-operator">[</span><span class="hljs-string">&quot;user.create&quot;</span>, <span class="hljs-string">&quot;user.delete&quot;</span>, <span class="hljs-string">&quot;user.get&quot;</span>, <span class="hljs-string">&quot;user.list&quot;</span>, <span class="hljs-string">&quot;project.create&quot;</span>, ...<span class="hljs-operator">]</span>
</code></pre>
<h3 id="benefits-for-agents">Benefits for Agents</h3>
<ul>
<li>No external docs needed: agents can query the CLI directly and construct accurate inputs</li>
<li>Auto-adapts to API changes: when the CLI updates, the schema updates with it, so agents always work against the latest spec</li>
<li>Pairs with dry-run: build input from the schema, validate with <code>--dry-run</code>, then execute</li>
<li>Self-describing: the CLI can describe itself without needing a separate AGENTS.md or tool description</li>
</ul>
<h3 id="our-implementation-overview">Our Implementation Overview</h3>
<p>In our project, we implemented it with the following structure:</p>
<ol>
<li>Define input/output schemas on each command handler (based on Pydantic models)</li>
<li>The <code>schema</code> subcommand serializes these into JSON Schema and returns them</li>
<li>A <code>--list</code> option allows browsing the full resource/action tree</li>
<li>Include an <code>examples</code> field in schema responses so agents have something to reference</li>
</ol>
<p>The implementation itself wasn't particularly difficult. Since we were already defining input/output models with Pydantic, most of it was solved just by calling <code>.model_json_schema()</code>.</p>
<h3 id="example-agent-workflow-using-schema">Example Agent Workflow Using Schema</h3>
<p>Here's what the actual flow looks like when an agent uses the schema:</p>
<pre><code>1. Agent: mytool schema --list
   → Check available commands

2. Agent: mytool schema user.create
   → Check input schema (required fields: email, role)

3. Agent: mytool user create --json '{&quot;email&quot;:&quot;new@example.com&quot;,&quot;role&quot;:&quot;member&quot;}' --dry-run
   → Validate before execution

4. Agent: mytool user create --json '{&quot;email&quot;:&quot;new@example.com&quot;,&quot;role&quot;:&quot;member&quot;}'
   → Execute, receive JSON response

5. Agent: Use the id field from the response for the next task
</code></pre>
<p>At no point in this flow does the agent touch the documentation. The CLI itself acts as the docs.</p>
<h2 id="practical-design-patterns">Practical Design Patterns</h2>
<p>Here are some patterns I've put together for actually applying JSON I/O and schema.</p>
<h3 id="input-design-choices">Input Design Choices</h3>
<p>There are roughly three ways to take input, and you can pick depending on the situation.</p>
<table>
<thead>
<tr>
<th>Approach</th>
<th>Example</th>
<th>Best For</th>
</tr>
</thead>
<tbody>
<tr>
<td>stdin JSON</td>
<td><code>echo &#39;{&quot;key&quot;:&quot;val&quot;}&#39; | mytool create</code></td>
<td>Large payloads, pipeline chaining</td>
</tr>
<tr>
<td>argument JSON</td>
<td><code>mytool create --json &#39;{&quot;key&quot;:&quot;val&quot;}&#39;</code></td>
<td>Single command execution, keeping it in shell history</td>
</tr>
<tr>
<td>Mixed</td>
<td><code>mytool create --name foo --json &#39;{&quot;extra&quot;:&quot;opts&quot;}&#39;</code></td>
<td>Frequently used options as flags, the rest as JSON</td>
</tr>
</tbody>
</table>
<p>Personally, I'd recommend argument JSON as the default with stdin support as well. From an agent's perspective, a self-contained single command is the easiest to work with.</p>
<h3 id="output-design-best-practices">Output Design Best Practices</h3>
<p>A few important principles for output design:</p>
<ul>
<li><code>--json</code> flag: keep the default human-readable, but return structured output when <code>--json</code> is passed</li>
<li>NDJSON support: for streaming scenarios (logs, events, etc.), support line-delimited JSON</li>
<li>Errors in JSON too: in <code>--json</code> mode, errors should also be returned as JSON, alongside exit codes</li>
<li>Include metadata: pagination info, request IDs, timestamps should be part of the response</li>
</ul>
<pre><code class="language-bash hljs"><span class="hljs-comment"># Normal mode</span>
$ mytool user list
EMAIL              ROLE     CREATED
alice@example.com  admin    2026-01-15
bob@example.com    member   2026-02-20

<span class="hljs-comment"># JSON mode</span>
$ mytool user list --json
<span class="hljs-operator">{</span>
  <span class="hljs-string">&quot;data&quot;</span>: <span class="hljs-operator">[</span>
    <span class="hljs-operator">{</span><span class="hljs-string">&quot;email&quot;</span>: <span class="hljs-string">&quot;alice@example.com&quot;</span>, <span class="hljs-string">&quot;role&quot;</span>: <span class="hljs-string">&quot;admin&quot;</span>, <span class="hljs-string">&quot;created_at&quot;</span>: <span class="hljs-string">&quot;2026-01-15T00:00:00Z&quot;</span><span class="hljs-operator">}</span>,
    <span class="hljs-operator">{</span><span class="hljs-string">&quot;email&quot;</span>: <span class="hljs-string">&quot;bob@example.com&quot;</span>, <span class="hljs-string">&quot;role&quot;</span>: <span class="hljs-string">&quot;member&quot;</span>, <span class="hljs-string">&quot;created_at&quot;</span>: <span class="hljs-string">&quot;2026-02-20T00:00:00Z&quot;</span><span class="hljs-operator">}</span>
  <span class="hljs-operator">]</span>,
  <span class="hljs-string">&quot;meta&quot;</span>: <span class="hljs-operator">{</span><span class="hljs-string">&quot;total&quot;</span>: 2, <span class="hljs-string">&quot;page&quot;</span>: 1, <span class="hljs-string">&quot;per_page&quot;</span>: 50<span class="hljs-operator">}</span>
<span class="hljs-operator">}</span>
</code></pre>
<p>In the end, I actually went with JSON output as the default and added a <code>--no-json</code> flag instead. If the tool isn't meant for human use, unifying all I/O as JSON gave us the best hit rate.</p>
<h3 id="versioning-backward-compatibility">Versioning &amp; Backward Compatibility</h3>
<p>Versioning JSON output needs some care. A few strategies:</p>
<ul>
<li>Adding fields is safe, removing/changing is not: adding new fields doesn't break backward compatibility, but removing fields or changing types can break agents</li>
<li>Include a version field: putting something like <code>&quot;api_version&quot;: &quot;v1&quot;</code> in the response lets agents branch based on version</li>
<li>Deprecation warnings: fields slated for removal should be flagged in a separate warnings array</li>
</ul>
<h3 id="using-pydantic-zod-json-schema-for-validation">Using Pydantic / Zod / JSON Schema for Validation</h3>
<p>Some tools for schema definition:</p>
<ul>
<li><strong>Python</strong>: Pydantic is the most convenient. Model definition → automatic JSON Schema generation → input validation, all in one</li>
<li><strong>TypeScript/Node</strong>: Define schemas with Zod and convert using <code>zod-to-json-schema</code></li>
<li><strong>Go/Rust etc.</strong>: Write JSON Schema files directly or use code-generation libraries</li>
</ul>
<p>The key point is that the type definitions used in your code and the schema returned by the schema command must come from the same source. If these are managed separately, they eventually drift out of sync.</p>
<blockquote>
<p>Honestly, we didn't pay much attention to this in our own project. It cost us quite a few failures.</p>
</blockquote>
<h3 id="ai-friendly-helper-flags">AI-Friendly Helper Flags</h3>
<p>Beyond schema and JSON, a few more agent-friendly flags worth adding:</p>
<ul>
<li><code>--dry-run</code>: preview results without actually executing. Lets agents safely test things out</li>
<li><code>--explain</code>: describe what the command will do in natural language. Helps with agent planning</li>
<li><code>--output-format</code>: choose between json, yaml, csv, etc.</li>
<li><code>--quiet</code>: strip unnecessary banners and warnings, return only essential output</li>
<li><code>--no-color</code>: remove ANSI escape codes (honestly, every CLI should have this)</li>
</ul>
<h2 id="results-lessons-and-caveats-after-adoption">Results, Lessons, and Caveats After Adoption</h2>
<h3 id="quantitative-qualitative-outcomes">Quantitative &amp; Qualitative Outcomes</h3>
<p>I shared the JSON transition results earlier. Here's what changed after adding the schema command:</p>
<table>
<thead>
<tr>
<th>Metric</th>
<th>JSON Only</th>
<th>JSON + Schema</th>
</tr>
</thead>
<tbody>
<tr>
<td>Task success rate</td>
<td>Around 90%</td>
<td>~97% (almost all succeeded)</td>
</tr>
<tr>
<td>Agent first-try accuracy</td>
<td>~70%</td>
<td>~90% (honestly, this was the biggest improvement)</td>
</tr>
<tr>
<td>Doc references needed</td>
<td>Avg 1.2 per task</td>
<td>Nearly 0</td>
</tr>
</tbody>
</table>
<p>The sample size wasn't huge, so the numbers may not mean much on their own. What I felt more strongly was how much simpler the agent code got. The parsing logic disappeared and we could focus on business logic.</p>
<h3 id="common-failure-patterns-we-observed">Common Failure Patterns We Observed</h3>
<p>It doesn't solve everything, though. Here are the patterns where agents kept failing, and what fixed them:</p>
<ul>
<li>Oversized JSON responses: when a list API returns thousands of items, it blows the agent's context window. Pagination and filtering are essential</li>
<li>Deeply nested structures: JSON nested 5+ levels deep is hard for agents to navigate accurately. Keep it flat when possible</li>
<li>Enum value errors: even with enums defined in the schema, agents sometimes insert similar but incorrect values. Input validation + clear error messages help</li>
<li>optional vs required confusion: agents sometimes skip required fields. Mark required fields clearly in the schema and tell them which fields are missing in error messages</li>
</ul>
<h3 id="remaining-challenges">Remaining Challenges</h3>
<p>Some unsolved problems remain:</p>
<ul>
<li>Complex pagination: getting agents to handle cursor-based pagination smoothly remains tricky</li>
<li>Binary data: file uploads/downloads and other binary data are hard to express cleanly in JSON</li>
<li>Long-running operations: tracking status and handling timeouts for tasks that take several minutes</li>
</ul>
<p>I still haven't found great answers for these. For long-running operations especially, most agents end up polling with their own sleep loops, which is pretty inefficient. Ideally, agents should be able to receive callbacks, but that's not easy to pull off.</p>
<h2 id="conclusion">Conclusion</h2>
<p>To sum up, for me building an AI-friendly CLI comes down to two things.</p>
<ol>
<li>JSON-First I/O: structure your inputs and outputs so agents can parse and use them reliably</li>
<li>Schema Command: let the CLI describe its own interface, eliminating the dependency on external docs</li>
</ol>
<p>I saw a real jump in agent performance from just these two. If you want to try it right away, the easiest place to start is adding a single <code>--json</code> flag to your existing CLI. That alone makes working with agents a lot smoother.</p>
<p>The era of the CLI has come around again.</p>
<p><img loading="lazy" src="https://www.hahwul.com/posts/2026/building-ai-friendly-clis/cli.webp" alt="" /></p>
]]></content:encoded>
      <pubDate>Sun, 22 Mar 2026 00:00:00 +0000</pubDate>
      <category>ai</category>
      <category>cli</category>
      <category>development</category>
    </item>
    <item>
      <title>10 Years of Reflection and a New Beginning</title>
      <link>https://www.hahwul.com/posts/2026/10years/</link>
      <guid>https://www.hahwul.com/posts/2026/10years/</guid>
      <description>10 years of hahwul.com. Looking back and sharing what’s next.</description>
      <content:encoded><![CDATA[<p>Hello everyone! This is my first post of 2026. I wanted to publish this back in January, but various things kept piling up and the work took longer than expected, so I'm finally getting it out now.</p>
<p>As we hit 2026, there's a fun fact: it's been exactly 10 years since I started using this domain and going by the name hahwul. It's a long time when I think about it, but it went by surprisingly fast. Today, along with looking back on these 10 years, I also have some fairly big changes to share regarding the blog's operation, content, and overall direction.</p>
<pre><code>Domain Name: hahwul.com
Registry Domain ID: 1992489747_DOMAIN_COM-VRSN
...
Updated Date: 2026-01-05T20:29:07Z
Creation Date: 2016-01-07T23:55:03Z
</code></pre>
<h2 id="10-years-ago">10 Years Ago</h2>
<p>Actually, even before the name hahwul, I ran another domain for about two years, and before that I had a different blog back when I was a kid. So I've been writing online for over 15 years now. Those early blogs were just dev notes where I jotted things down, though. What got me to where I am today is the name hahwul and this blog.</p>
<p>I made up the name hahwul from my own name, and over these 10 years I've written a huge number of posts under it. Even after a couple of big cleanups, it comes to around 1,400 articles. Most were about security and development, mixed with my own worries and thoughts. Writing things down really makes them stick, and I think those were the moments I learned the most.</p>
<p>I don't think I could have kept writing all this time on persistence alone. It was possible because so many of you kept coming back to read. Thank you!</p>
<div class="images-full-width">


<div class="images-grid">
    
    <div class="images-grid-item">
        <img src="https://www.hahwul.com/posts/2026/10years/images/1.png" alt="" loading="lazy">
    </div>
    
    <div class="images-grid-item">
        <img src="https://www.hahwul.com/posts/2026/10years/images/2.png" alt="" loading="lazy">
    </div>
    
    <div class="images-grid-item">
        <img src="https://www.hahwul.com/posts/2026/10years/images/3.png" alt="" loading="lazy">
    </div>
    
</div>
</div>
<h2 id="announcement">Announcement</h2>
<p>I have one announcement regarding the direction of my content. While the blog has been more of a general resource covering all sorts of information until now, I want to shift toward sharing more of my own stories and thoughts.</p>
<p>That doesn't mean the kind of posts I've been writing will disappear, of course. They'll just be reorganized. Posts that are too outdated or too thin will be removed, and the rest will be rebuilt into something more solid.</p>
<p>The plan is to leave thoughts and opinions on technology under <strong>Posts</strong> now and then, and to keep more structured, organized write-ups under <strong>Notes</strong>. And if I have the energy, I'd like to add some series-style content too (e.g., a ZAP guide).</p>
<h2 id="conclusion">Conclusion</h2>
<p>Anyway, I want to say thank you once again. I'll keep writing steadily, so I hope you'll keep following along!</p>
<p><img loading="lazy" src="https://www.hahwul.com/posts/2026/10years/images/2026.jpg" alt="" /></p>
]]></content:encoded>
      <pubDate>Sun, 22 Feb 2026 00:00:00 +0000</pubDate>
      <category>announcement</category>
    </item>
    <item>
      <title>Remove co-authored-by when committing</title>
      <link>https://www.hahwul.com/notes/claude-code/remove-co-authored-by/</link>
      <guid>https://www.hahwul.com/notes/claude-code/remove-co-authored-by/</guid>
      <description>Claude Code에서 커밋 시 co-authored-by를 남기지 않도록 설정하는 방법</description>
      <content:encoded><![CDATA[<p><code>~/.claude/settings.json</code>에 attribution 내 항목을 비워두면 커밋/PR 시 co-authored-by가 자동으로 추가되지 않음</p>
<pre><code class="language-json hljs"><span class="hljs-punctuation">{</span>
  <span class="hljs-name">&quot;attribution&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-name">&quot;commit&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-name">&quot;pr&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&quot;</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<p>hook을 이용해서도 제거할 수 있음 <code>~/.git-hooks/commit-msg</code> 와 같이 추가하여 처리 가능함</p>
<pre><code class="language-bash hljs"><span class="hljs-meta">#!/bin/sh
</span><span class="hljs-comment"># Co-Authored-By, Claude-Session, Generated with 등 제거</span>
sed -i.bak -e <span class="hljs-string">&#39;/^Co-Authored-By:/d&#39;</span> <span class="hljs-string">\
</span>           -e <span class="hljs-string">&#39;/^Claude-Session:/d&#39;</span> <span class="hljs-string">\
</span>           -e <span class="hljs-string">&#39;/Generated with Claude Code/d&#39;</span> <span class="hljs-string">\
</span>           -e <span class="hljs-string">&#39;/🤖 Generated with/d&#39;</span> <span class="hljs-string">&quot;</span><span class="hljs-variable">$1</span><span class="hljs-string">&quot;</span>
rm -f <span class="hljs-string">&quot;</span><span class="hljs-variable">$1</span><span class="hljs-string">.bak&quot;</span>
</code></pre>
]]></content:encoded>
    </item>
  </channel>
</rss>
