Bots
Bot detection is deliberately aggressive: a user agent is a bot as soon as one matcher says so, unless it is a known exception.
The lists
bots.yml, keyed by a lowercase substring of the user agentbot_categories.yml: ai_crawler, search_engine_crawler, scanner…search_engines.yml: answer bot.search_engine?bot_exceptions.yml: never a bot, whatever a matcher saysbrowser.bot.name is the name from bots.yml, or Generic Bot when another matcher caught it. The lists are in the repository.
Every week, script/sync_bots.rb rewrites the part of bots.yml below its marker line, and bot_categories.yml, from crawler-user-agents and ai.robots.txt, and a pull request shows the answers that change. Above the marker, the list inherited from browser stays as it was.
Coverage
How many of the bots of crawler-user-agents and ai.robots.txt each gem calls a bot, per category: every user agent crawler-user-agents gives as an instance, and, for each name of ai.robots.txt, which gives none, the user agent an AI agent of 2026 sends. script/bot_coverage.rb counts them into docs/bot_coverage.json and the weekly sync regenerates it, with this table.
Of the 882 crawler user agents added to crawler-user-agents since 2024 (test drivers and scanners left out), browser misses 353 and Agentry 11. Against fnando/browser dd88933.
| Category | User agents | browser: bot? | Agentry: bot? | Agentry: a category |
|---|---|---|---|---|
academic_research | 35 | 24 | 35 | 34 |
advertising_and_marketing | 97 | 78 | 94 | 94 |
ai_agent | 35 | 12 | 21 | 28 |
ai_assistant | 35 | 19 | 34 | 34 |
ai_crawler | 186 | 150 | 183 | 183 |
ai_search | 45 | 36 | 44 | 43 |
archiver | 68 | 50 | 68 | 68 |
browser_automation | 23 | 2 | 2 | 21 |
feed_fetcher | 92 | 40 | 91 | 91 |
http_library | 103 | 68 | 102 | 102 |
monitoring_and_analytics | 260 | 141 | 258 | 256 |
page_preview | 136 | 110 | 135 | 133 |
scanner | 108 | 46 | 106 | 106 |
search_engine_crawler | 418 | 384 | 417 | 406 |
search_engine_optimization | 685 | 481 | 674 | 671 |
| Total | 2,326 | 1,641 | 2,264 | 2,270 |
browser_automation is never bot? on purpose (below). ai_agent misses its plain-word names (Operator, Cursor, Devin, Codex): as substrings they would match human user agents.
Categories
browser.bot.category names what a bot declares it is, with the names of Cloudflare’s verified bot categories where one fits, so an app can compare it with what Cloudflare verified. It is nil for a browser, a bot exception, and a bot no list names (caught by a keyword).
browser = Browser.new("Mozilla/5.0 (compatible; Claude-User/1.0)")
browser.bot.category # => :ai_assistant
browser.bot.ai? # => true: ai_crawler, ai_search, ai_assistant or ai_agent
browser.bot.user_triggered? # => true: ai_assistant or ai_agent, acting for a person right now
| Category | bot? |
|---|---|
search_engine_crawler, search_engine_optimization, ai_crawler, ai_search, ai_assistant, ai_agent, page_preview, feed_fetcher, advertising_and_marketing, monitoring_and_analytics, archiver, academic_research, security | true |
http_library: curl, got, python-requests… | true, as curl and python-requests already were in browser: system tests don’t send them. |
scanner: sqlmap, Nikto, Censys… | true |
browser_automation: HeadlessChrome, Playwright, Puppeteer, Selenium… | false: the system tests of an app drive a real browser through them, and Playwright’s and Selenium’s headless Chrome send HeadlessChrome. PhantomJS stays a bot, as in browser. |
To treat test drivers as bots in production: browser.bot? || browser.bot.category == :browser_automation.
Matchers
| Matcher | A bot when |
|---|---|
Browser::Bot::EmptyUserAgentMatcher | The user agent is empty. |
Browser::Bot::KnownBotsMatcher | The user agent contains a key of bots.yml. |
Browser::Bot::KeywordMatcher | The user agent matches /crawl|fetch|search|monitoring|spider|bot/. |
browser.bot.why? returns the matcher that fired. A matcher is any callable taking the lowercased user agent and a browser:
Browser::Bot.matchers << ->(ua, _browser) { ua.match?(/externalhit/i) }
To start from nothing, Browser::Bot.matchers.clear; to get the built-in ones back, Browser::Bot.matchers += Browser::Bot.default_matchers. To restore browser 2’s detection, remove the keyword and empty user agent matchers:
Browser::Bot.matchers.delete(Browser::Bot::KeywordMatcher)
Browser::Bot.matchers.delete(Browser::Bot::EmptyUserAgentMatcher)
Extending the lists
Browser::Bot.bots.merge!(new_bots_hash)
Browser::Bot.bot_exceptions += new_exceptions
Browser::Bot.search_engines.merge!(new_search_engines_hash)
The prefix tries behind each list are rebuilt when the list grows or is replaced. Browser::Bot.category_by_keyword takes new categories the same way. To add a bot to Agentry itself, add its user agent to test/ua_bots.yml and its name to bots.yml; a search engine also goes in test/ua_search_engines.yml and search_engines.yml.
A trap. Public crawler lists call “bot” anything that is not a human browser: crawlers, but also test drivers (Playwright, Puppeteer, Selenium), scripts and scanners. Adding them all to bot? would flag the system tests of the apps using it, so test drivers get a category of their own and no bot?.
The corpus
corpus/user_agents.txt holds 20,328 distinct user agents. A change that is not meant to change an answer proves it there: one script writes every answer of a gem as JSON lines, another diffs two snapshots.
ruby -I path/to/upstream/lib script/corpus_snapshot.rb > /tmp/upstream.jsonl
ruby -I lib script/corpus_snapshot.rb > /tmp/agentry.jsonl
ruby script/corpus_diff.rb /tmp/upstream.jsonl /tmp/agentry.jsonl
path/to/upstream is a plain clone of fnando/browser at dd88933. A change meant to change answers, like a fresher bot list, lists the differences it expects in corpus/expected_differences.jsonl (corpus_diff.rb --write), and the diff must show exactly those. No browser of uap-core turns into a bot: the sync leaves out any keyword that would flag one.
Data sources
The corpus gathers, deduplicated, the user agents of:
- the test cases of ua-parser/uap-core, Apache License 2.0;
- the
instancesof monperrus/crawler-user-agents, MIT; - the test fixtures of fnando/browser, MIT.
The bot list and its categories are synced from permissively licensed sources only: crawler-user-agents and ai.robots.txt, both MIT. script/bot_coverage.rb counts, per category, the bots of both lists that browser and Agentry detect, into docs/bot_coverage.json. Details in corpus/SOURCES.md.