Skip to content

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
end

2. 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
end

3. 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
end

4. 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
end

6. 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
end

Key points

Framework comparison — Ruby ecosystem

FrameworkMiddleware hookShort-circuitHeader key
Hanamiconfig.middleware.use[403, headers, body]HTTP_USER_AGENT
Railsconfig.middleware.use[403, headers, body]HTTP_USER_AGENT
Sinatrabefore filter / usehalt 403request.user_agent
Padrinobefore filter (Sinatra-based)halt 403request.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