{"slug": "how-i-built-three-dry-cli-extensions-then-the-forum-s-ai-bot-thought-i-was-a", "title": "How I built three dry-cli extensions, then the forum's AI bot thought I was a spammer", "summary": "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.", "body_md": "[// Open Source](https://kig.re/categories/Open%20Source)\n\n# How I built three dry-cli extensions, then the forum's AI bot thought I was a spammer\n\nNOTE\n\nThe 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](#original-help).\n\nAt about 1 a.m., I announced three new Ruby gems on the dry-rb forum. The forum hid the post and silenced my account.\n\nI 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](https://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.\n\nThat 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.\n\n## The same chores, one CLI after another\n\nI’ve written a lot of command-line tools in Ruby. I like [dry-cli](https://github.com/dry-rb/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.\n\nI [wrote about using dry-cli](https://kig.re/2020/09/07/writing-cli-tools-ruby-migrating-github-issues-to-pivotal-tracker.html) in [githuh](https://rubygems.org/gems/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:\n\nMy [`dss` command](https://rubygems.org/gems/datadog-statsd-schema) analyzes metric schemas before they turn into a Datadog bill. [flowengine-cli](https://rubygems.org/gems/flowengine-cli) runs and validates flow definitions. More recently, [`dmez`](https://rubygems.org/gems/dnsmadeeasy) got an `export`, `plan`, and `apply` workflow for DNS zone files.\n\nDifferent 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.\n\nThen 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.\n\nNext 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.\n\nThe help work became `dry-cli-help`. This is what it changes:\n\nIt 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:\n\n```\nrequire \"dry/cli/help\"\n\nDry::CLI::Help.configure do\n  title \"MyCLI\"\n  description \"Compile, validate, and evaluate rules.\"\n  color :auto\n  wrap true\nend\n```\n\nThe 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:\n\n## When a command takes a while\n\nFor a slow command, I want to know what it’s doing. If it’s downloading 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:\n\n``` python\nclass Import < Dry::CLI::Command\n  include Dry::CLI::UI\n\n  def call(**)\n    rules = ui.spinner(\"Loading tax rules\") { load_rules }\n\n    ui.progress(\"Importing rules\", total: rules.size) do |bar|\n      rules.each { |rule| import(rule); bar.advance }\n    end\n\n    ui.success \"Imported #{rules.size} rules\"\n  rescue => e\n    ui.error(\"Import failed\", e.message)\n  end\nend\n```\n\nThe gem uses Piotr Murach’s [TTY Toolkit](https://ttytoolkit.org) 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.\n\nHere’s a run that downloads 64 IRS forms, six at a time:\n\nAnd here’s one that probes addresses on a local /24 network, ten at a time:\n\nThe [form download](https://asciinema.org/a/Il7LOQSahRD1ZhHV) and [host scan](https://asciinema.org/a/gBU8BS3KCRp97FX1) recordings are also on asciinema. They use the `main` branch. The `-c` concurrency option in the recordings arrived after version 0.5.0.\n\n## The Tab key should know the commands\n\nShell 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.\n\n`dry-cli-autocomplete` does that. Register a completion command:\n\n```\nrequire \"dry/cli/autocomplete/command\"\nregister \"completion\", Dry::CLI::Autocomplete::Command[MyCLI]\n```\n\nThen generate a script for your shell:\n\n```\nmycli completion bash > /usr/local/etc/bash_completion.d/mycli\n```\n\nNow 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.\n\n[`dry-cli-completion` already exists](https://github.com/rngtng/dry-cli-completion). 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.\n\n## Back to the forum\n\nBy 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:\n\n```\ngem install dry-cli-help dry-cli-ui dry-cli-autocomplete\n```\n\nI 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.\n\nSo there I was at 1 a.m. with a hidden announcement and a silenced account. I [wrote about it on the Hanakai forum](https://discourse.hanakai.org/t/false-positive-spam-detection-on-dry-forum-for-post-about-dry-cli-tools/1536), feeling pretty irritated. Seventeen minutes later, [Tim Riley](https://github.com/timriley) replied. He explained why the filter had fired, marked the post as legitimate, and restored it.\n\nThen 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.\n\nThe CLI behind the screenshots is `tilda`, part of [agentilda](https://github.com/kigster/agentilda-ai-setup). 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](https://dry-cli.tools).", "url": "https://wpnews.pro/news/how-i-built-three-dry-cli-extensions-then-the-forum-s-ai-bot-thought-i-was-a", "canonical_source": "https://kig.re/2026/09/17/2026-09-17--how-i-build-three-gem-extensions-and-got-ai-banned.html", "published_at": "2026-09-17 00:00:00+00:00", "updated_at": "2026-09-17 22:53:43.212594+00:00", "lang": "en", "topics": ["developer-tools", "ai-crawlers"], "entities": ["dry-rb", "dry-cli", "dry-cli-help", "dry-cli-ui", "dry-cli.tools", "Ruby", "Hanami", "githuh"], "alternates": {"html": "https://wpnews.pro/news/how-i-built-three-dry-cli-extensions-then-the-forum-s-ai-bot-thought-i-was-a", "markdown": "https://wpnews.pro/news/how-i-built-three-dry-cli-extensions-then-the-forum-s-ai-bot-thought-i-was-a.md", "text": "https://wpnews.pro/news/how-i-built-three-dry-cli-extensions-then-the-forum-s-ai-bot-thought-i-was-a.txt", "jsonld": "https://wpnews.pro/news/how-i-built-three-dry-cli-extensions-then-the-forum-s-ai-bot-thought-i-was-a.jsonld"}}