How to Block AI Bots in Ruby Hanami
Hanami is a Rack-based Ruby framework, so AI bot blocking follows the same Rack middleware pattern used across the Ruby ecosystem. A single middleware class intercepts every request before it reaches your actions, checks the User-Agent, and either returns a 403 immediately or adds the X-Robots-Tag header to passing responses.
1. Bot pattern list
Create a shared module so the same pattern list can be used in middleware, per-action guards, or specs without duplication.
# lib/ai_bots.rb
module AiBots
PATTERNS = %w[
gptbot
chatgpt-user
claudebot
anthropic-ai
ccbot
google-extended
cohere-ai
meta-externalagent
bytespider
omgili
diffbot
imagesiftbot
magpie-crawler
amazonbot
dataprovider
netcraft
].freeze
def self.ai_bot?(user_agent)
ua = user_agent.to_s.downcase
PATTERNS.any? { |pattern| ua.include?(pattern) }
end
end2. Rack middleware class
Rack passes the request environment as a hash. Headers are normalised to uppercase with an HTTP_ prefix — User-Agent becomes HTTP_USER_AGENT. Return a 3-element array to short-circuit; call @app.call(env) to continue.
# app/middleware/ai_bot_blocker.rb
require_relative "../../lib/ai_bots"
class AiBotBlocker
def initialize(app)
@app = app
end
def call(env)
# Rack normalises headers: User-Agent -> HTTP_USER_AGENT
user_agent = env["HTTP_USER_AGENT"] || ""
if AiBots.ai_bot?(user_agent)
[
403,
{
"Content-Type" => "text/plain",
"X-Robots-Tag" => "noai, noimageai",
},
["Forbidden — AI crawlers are not permitted on this site."],
]
else
status, headers, body = @app.call(env)
headers["X-Robots-Tag"] = "noai, noimageai"
[status, headers, body]
end
end
end3. Register in config/app.rb
Add the middleware to config/app.rb. Hanami stacks middleware in registration order — your blocker runs after Rack::Static (which serves public/) and before your routes.
# config/app.rb
require "hanami"
require_relative "../app/middleware/ai_bot_blocker"
module MyApp
class App < Hanami::App
config.middleware.use AiBotBlocker
# Other config...
end
end4. robots.txt — no bypass needed
Hanami automatically registers Rack::Static to serve files from public/ before any custom middleware. A public/robots.txt is always reachable regardless of User-Agent — no explicit path bypass required in your middleware.
# public/robots.txt is served by Rack::Static BEFORE middleware runs.
# Hanami automatically adds Rack::Static for the public/ directory.
# No special bypass logic needed — /robots.txt is always accessible.
#
# public/robots.txt:
User-agent: *
Allow: /
User-agent: GPTBot
Disallow: /
User-agent: ClaudeBot
Disallow: /
User-agent: CCBot
Disallow: /5. Per-action guard (alternative)
If you only want to protect specific actions rather than the whole app, check req.env['HTTP_USER_AGENT'] inside the action's handle method and set res.status directly.
# app/actions/articles/show.rb
# Per-action guard — use when middleware is too broad
module MyApp
module Actions
module Articles
class Show < MyApp::Action
def handle(req, res)
user_agent = req.env["HTTP_USER_AGENT"] || ""
if AiBots.ai_bot?(user_agent)
res.status = 403
res.headers["X-Robots-Tag"] = "noai, noimageai"
res.body = "Forbidden"
return
end
# Normal action logic...
article = ArticleRepository.new.find(req.params[:id])
res.render(:show, article: article)
end
end
end
end
end6. Slices (Hanami 2)
Hanami 2 supports slices — independent sub-applications with their own config. Register middleware at the app level for global coverage, or per-slice for targeted protection.
# In Hanami 2 with slices, register middleware per-slice:
# slices/api/config/slice.rb
module Api
class Slice < Hanami::Slice
config.middleware.use AiBotBlocker
end
end
# Or in the main app only (slices inherit unless overridden):
# config/app.rb
module MyApp
class App < Hanami::App
config.middleware.use AiBotBlocker
end
endKey points
- Header naming: Rack uppercases all headers and adds
HTTP_—User-Agent→HTTP_USER_AGENT. Never use the original HTTP header name in middleware. - Short-circuit: Return
[status, headers, body]directly — do not call@app.call(env). The body must respond toeach(Array satisfies this). - Pass-through: Mutate the headers hash from
@app.call(env)before returning — some Rack apps return frozen hashes, so useheaders.merge(...)if needed. - Middleware order: Rack::Static runs first (static files bypass middleware). Custom middleware runs before Hanami router and actions.
- Slices: In Hanami 2, middleware registered on the App applies to all slices unless a slice overrides it.
Framework comparison — Ruby ecosystem
| Framework | Middleware hook | Short-circuit | Header key |
|---|---|---|---|
| Hanami | config.middleware.use | [403, headers, body] | HTTP_USER_AGENT |
| Rails | config.middleware.use | [403, headers, body] | HTTP_USER_AGENT |
| Sinatra | before filter / use | halt 403 | request.user_agent |
| Padrino | before filter (Sinatra-based) | halt 403 | request.user_agent |
Hanami and Rails share identical Rack middleware registration API — the same AiBotBlocker class works unchanged in both. Sinatra and Padrino use halt in before-filters with a friendlier request.user_agent accessor.
Dependencies
No additional gems required. The implementation uses only Ruby stdlib (String#downcase, Enumerable#any?, String#include?) and the Rack env hash that Hanami already provides.
# Gemfile — no additions needed
gem "hanami", "~> 2.1" # already in your project