cd /news/developer-tools/how-i-built-three-dry-cli-extensions… · home topics developer-tools article
[ARTICLE · art-133134] src=kig.re ↗ pub= topic=developer-tools verified=true sentiment=↓ negative

How I built three dry-cli extensions, then the forum's AI bot thought I was a spammer

A developer announced three new Ruby gems on the dry-rb forum at about 1 a.m., and the forum's automated spam filter hid the post and silenced the account, mistaking the infrequent poster's pile of links for spam. The gems — dry-cli-help, dry-cli-ui, and a third extension — were extracted over weeks from the developer's own dry-cli command-line tools, including githuh, dss, flowengine-cli, dmez, and tilda, and published on dry-cli.tools. The developer said the filter "made a fairly understandable guess about me," describing the outcome as "not quite the launch I had pictured.

read6 min views1 publishedSep 17, 2026

// Open Source

NOTE

The image above shows the help screen of one of my CLI utilities, which will be the subject of a future post. Here it is shown to demonstrate the difference between what help screen looks like after requiring dry-cli-help gem, versus the default help screen shown below.

At about 1 a.m., I announced three new Ruby gems on the dry-rb forum. The forum hid the post and silenced my account.

I had spent weeks extracting the gems from a CLI I was building. I had written up examples, recorded demos, and put everything on dry-cli.tools. Then I posted the announcement where dry-cli users might actually see it. A spam filter saw an infrequent poster with a pile of links and made a fairly understandable guess about me.

That was not quite the launch I had pictured. Before I get to how it ended, here’s why I built the gems in the first place.

The same chores, one CLI after another #

I’ve written a lot of command-line tools in Ruby. I like dry-cli because a command is a class, options are declared in plain sight, and nested commands are easy to register. Hanami uses it too. I can get on with writing the command instead of negotiating with the framework.

I wrote about using dry-cli in githuh back in 2020. That tool can export GitHub issues, and its issue export command has enough options to make a cramped help screen annoying. Here’s what that looks like when the descriptions wrap:

My dss command analyzes metric schemas before they turn into a Datadog bill. flowengine-cli runs and validates flow definitions. More recently, dmez got an export, plan, and apply workflow for DNS zone files.

Different jobs, same pattern: once the command itself worked, I still had to make its help readable, show progress where the work took time, and teach the shell what could come next. By the time I started tilda, copying those pieces into one more project sounded less appealing than finally pulling them out.

Then I run the command and look at the help screen. There’s no title or description for the program. Long descriptions run across the terminal on one line. Commands appear in alphabetical order, even when I would explain them in a different order. So I fix the help output for that project.

Next comes a command that takes more than a few seconds. I add a spinner or progress bar and make sure it doesn’t print hundreds of carriage returns into a CI log. Then I add shell completion so mycli db <TAB> suggests a command instead of the files in my current directory. By the next CLI, I’m doing all of it again. Apparently I enjoy writing zsh completion just enough to forget how much I dislike writing it.

The help work became dry-cli-help. This is what it changes:

It wraps descriptions to the terminal width and adds color, a title, a description, and an epilogue. I can group commands and put them in an order that helps a person find the next step. The setup fits in a few lines:

require "dry/cli/help"

Dry::CLI::Help.configure do
  title "MyCLI"
  description "Compile, validate, and evaluate rules."
  color :auto
  wrap true
end

The image at the top of the post is the new help screen from tilda, the CLI that started this whole exercise. Here is its original dry-cli help screen for comparison:

When a command takes a while #

For a slow command, I want to know what it’s doing. If it’s down 64 PDF files, I also want to know whether file 63 is still moving. That work became dry-cli-ui, which gives a dry-cli command a ui object:

class Import < Dry::CLI::Command
  include Dry::CLI::UI

  def call(**)
    rules = ui.spinner(" tax rules") { load_rules }

    ui.progress("Importing rules", total: rules.size) do |bar|
      rules.each { |rule| import(rule); bar.advance }
    end

    ui.success "Imported #{rules.size} rules"
  rescue => e
    ui.error("Import failed", e.message)
  end
end

The gem uses Piotr Murach’s TTY Toolkit to draw spinners, bars, tables, and prompts. In a terminal, progress updates in place. In a file or CI log, it prints plain lines. I have stared at enough failed job logs to care deeply about that last part.

Here’s a run that downloads 64 IRS forms, six at a time:

And here’s one that probes addresses on a local /24 network, ten at a time:

The form download and host scan recordings are also on asciinema. They use the main branch. The -c concurrency option in the recordings arrived after version 0.5.0.

The Tab key should know the commands #

Shell completion was the third repeat offender. A dry-cli app already knows its commands, aliases, options, and enum values. I wanted to turn that information into a script once, install it, and stop thinking about completion every time I added a command.

dry-cli-autocomplete does that. Register a completion command:

require "dry/cli/autocomplete/command"
register "completion", Dry::CLI::Autocomplete::Command[MyCLI]

Then generate a script for your shell:

mycli completion bash > /usr/local/etc/bash_completion.d/mycli

Now pressing Tab uses the generated script; it doesn’t start Ruby each time. It supports bash 3.2 and newer and writes native zsh completion with option descriptions. Hidden commands stay hidden, and file arguments complete as files.

dry-cli-completion already exists. I tried to use it. In my apps, I needed completion for file arguments and for child commands under a command group. I also didn’t want the completion require adding time to every ordinary CLI run. That’s why I wrote another gem instead of pretending I had invented the idea.

Back to the forum #

By this point, I had three independent gems, all at version 0.5.0, MIT licensed, and requiring Ruby 4.0 or newer. They plug into an existing dry-cli app without changing its commands. You can install all three or pick one:

gem install dry-cli-help dry-cli-ui dry-cli-autocomplete

I should also be clear about where they came from. These are my extensions, not dry-rb projects or gems endorsed by its maintainers. I built them with Claude Code involved. Most of the Ruby is mine; Claude reviewed code, wrote commit messages, and wrote much of the zsh completion code. I reviewed the result.

So there I was at 1 a.m. with a hidden announcement and a silenced account. I wrote about it on the Hanakai forum, feeling pretty irritated. Seventeen minutes later, Tim Riley replied. He explained why the filter had fired, marked the post as legitimate, and restored it.

Then he invited me to send pull requests to bring the colored, wrapped help output into dry-cli 2.0 itself. I had gone from “the forum thinks I’m a spammer” to “would you like to contribute this upstream?” in less than half an hour. The filter made a mistake. The person behind it handled the mistake well. Thanks, Tim.

The CLI behind the screenshots is tilda, part of agentilda. I’ll tell that story in another post. If you have a dry-cli app and one of these annoyances sounds familiar, the gems and examples are at dry-cli.tools.

── more in #developer-tools 4 stories · sorted by recency
── more on @dry-rb 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/how-i-built-three-dr…] indexed:0 read:6min 2026-09-17 ·