Every session, a coding agent meets your Rails app like a stranger. It reads the Gemfile, greps routes.rb, trusts a db/schema.rb that may lag behind the migrations, and guesses the rest from training data. This burns tokens before the first line of work, and these guesses cause bugs. We’ve measured this. In Agents on Rails, the benchmark Evil Martians built for the Rails Foundation, the leading models pass most atomic Rails tasks (Claude Opus 5 and Claude Fable 5.1 both pass 58 of 63 runs), yet even the best reach for the Rails API the task turns on in only 41% of runs; the rest of the time, it writes its own version. Frontier models are good at Rails. They’d be much better if someone handed them what the framework and your gems already document. That’s why Evil Martians built Rails Hyperdrive. In this article, we’ll cover all you need to know.
Rails Hyperdrive is a development-only engine that lets any gem in your bundle ship agent knowledge, installed only when your Gemfile calls for it, with eight MCP (Model Context Protocol) tools on the side that answer live from your booted app. The engine ships no knowledge of its own. Instead, your bundle decides what your agent learns.
In this post, we’ll install Rails Hyperdrive, edit what it installs, watch an upgrade collide with those edits, hand the collision to an agent, and show gem authors how to plug in with nothing but markdown files and one gemspec line.
#
We’ll start from a fresh rails new app (Rails 8.1, SQLite, the default Gemfile and nothing else) and add two gems to the development group: the engine itself and one companion gem, that is, an ordinary gem whose payload is agent knowledge.
Then comes the init:
$ bundle add rails-hyperdrive layered-rails-skills --group=development
$ bin/rails hyperdrive:init
create .mcp.json
append .gitignore
append Gemfile
insert config/routes.rb
create .hyperdrive/config.yml
create .claude/skills/layered-rails/SKILL.md
[... 47 more skill files ...]
create .claude/agents/layered-rails-planner.md
create .claude/agents/layered-rails-reviewer.md
create .claude/commands/layered-rails-analyze-callbacks.md
[... 7 more command files ...]
create .hyperdrive/lock.yml
done hyperdrive initialized
Mount: /_hyperdrive (in config/routes.rb)
Server: 8 MCP tools at http://localhost:3000/_hyperdrive/mcp
Installed 1 skill, 0 guidelines, 2 agents, 8 commands
layered-rails-skills@3.1.0
skill layered-rails (+47 files)
agent layered-rails-planner
agent layered-rails-reviewer
command layered-rails-analyze
[... 7 more commands ...]
The engine has mounted an MCP server inside your dev server and it wrote .mcp.json so Claude Code picks it up automatically. The companion gem handled the other half: layered-rails-skills installed an architecture skill (SKILL.md plus 47 supporting files) into .claude/skills/, where your agent loads it on demand.
The skill is Vladimir Dementyev’s Layered Rails skill, the coding-agent distillation of Layered Design for Ruby on Rails Applications which has been shipped as a companion by the skill’s author.
But that skill didn’t come alone. Here’s the whole .claude/ tree after init:
$ tree -L 3 .claude
.claude
├── agents
│ ├── layered-rails-planner.md
│ └── layered-rails-reviewer.md
├── commands
│ ├── layered-rails-analyze-callbacks.md
│ ├── layered-rails-analyze-gods.md
│ ├── layered-rails-analyze-services.md
│ ├── layered-rails-analyze.md
│ ├── layered-rails-archspec.md
│ ├── layered-rails-plan.md
│ ├── layered-rails-review.md
│ └── layered-rails-spec-test.md
└── skills
└── layered-rails
├── examples
├── references
├── SKILL.md
└── workflows
8 directories, 11 files
The same init installed two sub-agents into .claude/agents/: layered-rails-reviewer, which checks code for layer violations, and layered-rails-planner, which drafts a phased adoption roadmap. It also dropped eight slash commands into .claude/commands/. Open one and you’ll find a few lines that route into a workflow file inside the installed skill:
$ cat .claude/commands/layered-rails-review.md
Standalone code review from a layered architecture perspective.
[... usage block omitted ...]
Read and follow [`skills/layered-rails/workflows/review.md`](../skills/layered-rails/workflows/review.md), applied to the diff or file path(s) given as arguments.
The knowledge lives in one place and the agents and commands are its entry points. To see what an entry point buys you, let’s give the demo app a deliberately fat OrdersController#create that sums line items, applies a promo code, sets the initial status, and logs large orders, all inline. And we’ll run the review command on it:
$ claude -p "/layered-rails-review app/controllers/orders_controller.rb"
## Layered Rails Review
### Files Reviewed
- `app/controllers/orders_controller.rb` (Presentation)
- Context read: `app/models/order.rb`, `app/models/line_item.rb` (Domain), `app/mailers/order_mailer.rb`, `app/views/orders/*`, `db/schema.rb`, `test/models/*`
### Layer Analysis
- **Layers touched:** Presentation → Domain (direct), Presentation → Infrastructure (mailer)
- **Data flow:** Violation detected — pricing/discount/status rules live in the Presentation layer
[... 2 more lines omitted ...]
### Findings
🔴 **Critical: Business logic in controller (domain calculation)**
Location: `app/controllers/orders_controller.rb:13-15`
[... the three quoted controller lines omitted ...]
**Problem:** Three domain rules — how a total is computed, what "SPRING10" means (10% off, minimum $100), and what state a new order starts in — are encoded in an HTTP handler. `Order` knows none of them. Any second creation path (admin console, API controller, Rails console, import job, a future `Order.create!` in a test) produces an order with `total_cents = nil` and `status = nil`. The schema permits exactly that: both columns are nullable with no default and no validation.
**Fix:** Move the rules to `Order`; the controller keeps params extraction and response.
[... fix code, 2 warnings, and 3 suggestions omitted ...]
### Summary
[... "Good" list omitted ...]
**Needs Attention:**
1. 🔴 Total, discount, and initial status are domain rules stranded in `create` — move to `Order`
2. ⚠️ `Order` is anemic and untested as a result
3. ⚠️ Nil `price_cents`/` quantity` raises `NoMethodError` before validation runs
4. 💡 Promo catalog → value object; `large?` → model predicate
One fat action in, a prioritized findings list out, produced by the workflow the command points at. That’s what the artifacts are for; the skill holds the knowledge, and the commands and agents put it to work.
That’s three of the four artifact kinds a companion can install. The fourth, guidelines, showed up in the init summary as a zero; we’ll meet a real one later. These kinds differ in exactly one dimension, namely, who activates the content, and when:
| Kind | Who activates it (and when) | Install destination |
|---|---|---|
| Skill | Lazy: the agent loads it when the task matches its description—costs nothing until then | .claude/skills/ |
| Agent | Delegated: a specialist takes a bounded job off the main session’s hands | .claude/agents/ |
| Command | Invoked: you type /layered-rails-analyze , that workflow runs now |
.claude/commands/ |
| Guideline | Eager: short declarative rules in the agent’s context at all times | CLAUDE.md , via@ -import |
Everything a companion installs shows up in git status and nothing ends up gitignored except the engine’s own cache for the discover command we’ll meet later. The .hyperdrive/ directory holds the engine’s two bookkeeping files, a settings file that’s yours and a lock that’s the engine’s; we’ll open both when an upgrade forces the question. Let’s look at what we got:
$ git status --short
M .gitignore
M Gemfile
M config/routes.rb
?? .claude/
?? .hyperdrive/
?? .mcp.json
The MCP server is live the moment you boot bin/rails server. The agent’s tools answer from the running process; they ask the router instead of grepping routes.rb and read the live database schema instead of trusting db/schema.rb. Here’s the tool list straight from the endpoint:
$ curl -s http://localhost:3000/_hyperdrive/mcp \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq -r '.result.tools[].name'
describe_app
run_ruby
run_sql
tail_logs
list_models
locate_source
lookup_doc
list_routes
Either half is skippable: hyperdrive:init --skip-content sets up the MCP server without any content, and --skip-mcp does the reverse.
But live introspection is table stakes. The other half is the reason this project exists.
#
Package managers for agent knowledge already exist. npx skills installs any GitHub repo with a SKILL.md in it into your agent’s skills directory and Claude Code plugin marketplaces distribute versioned catalogs of skills and commands. Both leave you as the resolver: you find the knowledge, judge whether it fits your stack, and install it, whether or not you run the library the advice is about.
Laravel Boost goes further (it reads your composer.json and installs matching guidelines and skills) but the knowledge for the ecosystem’s major packages lives inside Boost itself, one curated repo maintained by the Laravel team, with per-version variants for the stack Laravel blesses: Livewire, Inertia, Pest.
That works because Laravel apps overwhelmingly run those defaults. Rails blesses defaults too (omakase, famously does this). But the same essay grants that “substitutions are allowed, within reason”, and the community exercises that right constantly. RSpec or Minitest. Sidekiq or Solid Queue or GoodJob. Hotwire or Inertia.
Every mature Rails app is a different intersection of choices, which means a single guideline pack would be wrong for most apps most of the time. An agent told about Minitest conventions in an RSpec codebase is actively worse than one given no guidance at all.
So Rails Hyperdrive ships zero content: no skills or guidelines go in your agent’s context window by default. What it ships is a contract. This means any gem can carry agent knowledge, declare what stack it targets, and the engine installs the intersection. Your bundle decides what your agent learns. This division of labor is convention over configuration, applied to agent knowledge. A companion gem with no manifest at all installs its content universally; a gem-root hyperdrive.yml manifest exists only to declare where your content deviates from “applies everywhere”. Every key is optional; you configure the exception, not the rule.
#
The layered-rails-skills companion shows why gating (installing a file only when the app’s bundle calls for it) earns its place. The skill ships reference manuals for nine specific gems: Alba, Action Policy, ViewComponent, and six more. The companion’s rule: advice that helps you pick a gem installs everywhere, but a reference manual for a gem you don’t bundle is dead weight and dangling links. So the nine manuals are gated, and our bare demo app got none of them: of the skill’s 57 markdown files (some 14,700 lines of guidance) exactly 48 hit.
Add two of those nine gems:
$ bundle add alba action_policy
[... Bundler output omitted ...]
$ bundle install
[... Bundler output omitted ...]
[hyperdrive] installed 2 artifact(s):
.claude/skills/layered-rails/references/gems/action-policy.md
.claude/skills/layered-rails/references/gems/alba.md
[hyperdrive] 1 artifact(s) need attention — run bin/rails hyperdrive:sync
.claude/skills/layered-rails/SKILL.md (layered-rails-skills@3.1.0 → layered-rails-skills@3.1.0)
The manuals arrived during bundle install itself, courtesy of a Bundler plugin that hyperdrive:init registered in the Gemfile. That’s why the transcript runs both commands: Bundler activates a newly declared plugin only on bundle install, so this first bundle add ran without it; from here on, bundle add triggers it too.
The plugin is additive-only and quiet by contract: it never rewrites an installed file and never fails your bundle. Anything beyond adding new files, it defers. Specifically, that’s the “need attention” line, and it’s why the arrow shows the same version on both sides: the gem didn’t move, your bundle did. Run the sync, and the skill’s entry file re-renders its gem-reference table:
$ bin/rails hyperdrive:sync
force .claude/skills/layered-rails/SKILL.md
[... 59 "unchanged" lines, lock update, and summary omitted ...]
$ git diff .claude/skills/layered-rails/SKILL.md
@@ -194,6 +194,8 @@ For library-specific guidance:
| Gem | Purpose | Reference |
|-----|---------|-----------|
+| action_policy | Authorization framework | [action-policy.md](references/gems/action-policy.md) |
+| alba | JSON serialization | [alba.md](references/gems/alba.md) |
| archspec | Enforce layer boundaries in CI (reference `Archspec.rb` config) | [archspec.md](references/gems/archspec.md) |
Two rows joined a table that already existed (the archspec row is ungated and was there from init) and each row lives only as long as its gem stays in the bundle. Now, bundle remove alba and sync again:
$ bin/rails hyperdrive:sync
force .claude/skills/layered-rails/SKILL.md
remove .claude/skills/layered-rails/references/gems/alba.md
[... lock update and summary omitted ...]
That force on SKILL.md is safe by construction. Rails Hyperdrive’s lock file (more on it shortly) records a content hash showing the file unedited, and a plain sync only overwrites files that still match what it installed. The manual and its table row are both gone, and no link in the skill dangles: your agent’s knowledge tracks your Gemfile in both directions.
#
Installed skills are plain files in your repo, and you will edit them (that’s the point of having them locally). This raises the question every dotfile manager, Rails generator, and config framework eventually faces: what happens when upstream ships a new version of a file you’ve changed?
To show the upgrade story we need an upstream that moves, so let’s write a toy companion gem. Here’s its entire content, gemspec aside:
rails-hyperdrive-sidekiq/
├── hyperdrive.yml
├── skills/sidekiq-idempotency/SKILL.md # or lib/<gem>/hyperdrive/skills/
└── lib/rails-hyperdrive-sidekiq/hyperdrive/guidelines/jobs-sidekiq.md
gem: sidekiq
The whole gem is two markdown files and a one-line manifest saying “only for apps that bundle Sidekiq.” The top-level gem: gates every artifact the gem ships; per-artifact keys exist only to deviate from it. The second file is our first guideline, the eager kind from the taxonomy, which layered-rails-skills didn’t ship. Skills sit at the gem root, where skills.sh users and plain git clones can see them; guidelines live at Rails Hyperdrive’s conventional path under lib/.
We point the demo app’s Gemfile at the toy gem and sync; the skill installs, and the guideline gets wired in through a single import chain; the @ prefix is Claude Code’s import syntax:
$ cat CLAUDE.md
<!-- AI instructions for this project. Managed content lives in .claude/hyperdrive/. -->
@.claude/hyperdrive/index.md
$ cat .claude/hyperdrive/index.md
@guidelines/jobs-sidekiq.md
Eager context isn’t free, and the sync says exactly what it costs:
eager 1 guideline(s), ~233 tokens always in context
Now, let’s return to the question this section opened with. Say, we add an app-specific warning to the installed skill (something like “payment jobs in this app must check Payment#processed_at before charging”) and upstream ships v0.2.0 with a new section on testing. The Bundler plugin spots the collision:
$ bundle install
Using rails-hyperdrive-sidekiq 0.2.0 (was 0.1.0) from source at `../gems/rails-hyperdrive-sidekiq-0.2.0`
[hyperdrive] 1 artifact(s) need attention — run bin/rails hyperdrive:sync
.claude/skills/sidekiq-idempotency/SKILL.md (rails-hyperdrive-sidekiq@0.1.0 → rails-hyperdrive-sidekiq@0.2.0)
A plain sync refuses to touch your work. That’s the default behavior:
$ bin/rails hyperdrive:sync
skip .claude/skills/sidekiq-idempotency/SKILL.md (locally modified; run hyperdrive:sync with --merge, --sidecar, or --overwrite to reconcile)
You pick the reconciliation strategy explicitly. --merge runs a git three-way merge between your file, the previously installed version, and the new upstream:
$ bin/rails hyperdrive:sync --merge
[... 49 "unchanged" lines omitted ...]
force .claude/skills/sidekiq-idempotency/SKILL.md
merged .claude/skills/sidekiq-idempotency/SKILL.md (local edits merged with rails-hyperdrive-sidekiq@0.2.0)
[... 12 "unchanged" lines, lock update, and summary omitted ...]
Merged 1 file by three-way merge; a clean merge is textually non-overlapping, not verified; review with `git diff`
The merged file carries both the payment warning and the new testing section, no conflict markers. Mind the footer, though: clean means textually non-overlapping, not semantically right (Git merges two contradictory edits without complaint when unchanged lines separate them) so git diff is the review step, not a formality.
And merges don’t always land even that cleanly. When edits collide, or when the previously installed gem version is gone from disk (a three-way merge needs it as the base), the same command never writes a half-merged file. It falls back to sidecar delivery and names the reason:
sidecar .claude/skills/sidekiq-idempotency/SKILL.md (locally modified; new upstream delivered to .claude/skills/sidekiq-idempotency/SKILL.md.new; conflicting edits)
You can also ask for that outcome directly with --sidecar. Your file stays untouched; the full new upstream lands next to it as SKILL.md.new, inert to the agent, visible in git status:
$ diff .claude/skills/sidekiq-idempotency/SKILL.md .claude/skills/sidekiq-idempotency/SKILL.md.new
25,28d24
< > **In this app:** payment jobs must additionally check
< > `Payment#processed_at` before charging — the gateway ledger is
< > reconciled nightly and double charges are expensive to unwind.
<
67a64,78
>
> ## Testing for idempotency
[...]
The pair is now yours to reconcile (or an agent’s task). An agent has both full texts, so it merges them by meaning rather than by line; a prompt naming the two paths is all it takes. There’s also a way to automate that handoff. --resolve works like git mergetool: after delivery, each unresolved .new goes to a command you name in .hyperdrive/config.yml, the settings file init created next to the lock:
resolve:
command: claude -p $PROMPT --allowedTools Read,Edit,Write
Take the collision that defeated git—upstream inserting a blockquote of its own right where our warning sits. --resolve delivers the sidecar and hands it straight to the command:
$ bin/rails hyperdrive:sync --resolve
[... 49 "unchanged" lines omitted ...]
create .claude/skills/sidekiq-idempotency/SKILL.md.new
sidecar .claude/skills/sidekiq-idempotency/SKILL.md (locally modified; new upstream delivered to .claude/skills/sidekiq-idempotency/SKILL.md.new)
[... 12 "unchanged" lines, lock update, and eager line omitted ...]
resolve .claude/skills/sidekiq-idempotency/SKILL.md via claude
remove .claude/skills/sidekiq-idempotency/SKILL.md.new
resolved .claude/skills/sidekiq-idempotency/SKILL.md
[... summary omitted ...]
Sidecars: 1 resolved
Here’s what Claude wrote, from the first hunk of the git diff. The two blockquotes that wanted the same spot now sit one under the other, upstream’s first; the second hunk is the new testing section at the bottom of the file.
$ git diff .claude/skills/sidekiq-idempotency/SKILL.md
@@ -22,6 +22,10 @@ state whether it runs once or five times.
change (or vanish) between enqueue and perform. Reload and bail out early
if the work is already done.
+> **Retry caps:** jobs with external side effects should set
+> `sidekiq_options retry: 5` and surface the terminal failure; 25 silent
+> retries against a payment gateway is an incident, not resilience.
+
> **In this app:** payment jobs must additionally check
> `Payment#processed_at` before charging — the gateway ledger is
> reconciled nightly and double charges are expensive to unwind.
[... second hunk omitted ...]
The flags combine too: --merge --resolve lets Git take what it merges cleanly and sends the command only what it couldn’t.
That $PROMPT stands for the prompt the agent runs with, rendered per sidecar from an ERB template that ships with the gem. To rewrite it, define your own:
resolve:
command: claude -p $PROMPT --allowedTools Read,Edit,Write
prompt: .hyperdrive/resolve-prompt.md.erb
Inside the template, the sidecar’s paths and provenance are available as methods, e.g. <%= local %> and <%= remote %>. The command line takes the same values as $ placeholders, which is how you’d wire up a tool that expects file paths instead of a prompt:
In command: |
In the template | Value |
|---|---|---|
$LOCAL |
local |
Your file, with your edits |
$REMOTE |
remote |
The sidecar: the new upstream |
$BASE |
base |
The upstream your edits were made on, when that gem version is still on disk (the lock remembers which one, even while a sidecar waits); nil otherwise |
$MERGED |
merged |
Your file again: the path the tool must write |
$SOURCE |
source |
<gem>@<version> of the new upstream |
$PREVIOUS_SOURCE |
previous_source |
<gem>@<version> your copy is based on |
$KIND |
kind |
skill ,guideline ,agent ,command , orskill_support |
$PROMPT |
— | The rendered template |
The $MERGED row is the contract. A sidecar is deleted only when the command exits 0 and that file changed, which is Git’s own mergetool default (trustExitCode = false). An agent that was denied every write, or that judged the merge beyond it, leaves the file untouched and gets reported as unresolved … (command exited 0 but wrote nothing); that, like any other exit, leaves the live file, the sidecar, and the lock exactly as they were, and the next --resolve hands the sidecar off again. The engine runs nothing you didn’t name, and never during bundle install. Either way you end up where --merge left you: with a git diff to review.
However a sidecar gets resolved, the lock records each upstream delivery exactly once, so nothing gets re-offered, and the file keeps showing as locally modified on future syncs—accurate, because you own it now.
The third strategy, --overwrite, discards your edits and restores gem-shipped content. Rails Hyperdrive tracks all of this through .hyperdrive/lock.yml, a git-tracked ledger—here trimmed to one delivered file:
version: 3
files:
- path: ".claude/skills/sidekiq-idempotency/SKILL.md"
artifact: skill
source: rails-hyperdrive-sidekiq@0.2.0
source_sha: 5efe4771dc42ace62920fb67a6335294328f5d2c0b2e84efeb6d5b14f160c498
installed_at: '2026-09-05T23:02:12Z'
source pins which gem delivered the file and at what version; source_sha is the hash of the delivered content. Comparing that hash against the file on disk is how a sync tells “unedited, safe to upgrade” from “edited, hands off” without asking you a single interactive question.
The lock holds state, and it’s the engine’s to write. Your choices live in the other file, the config.yml we put the resolver in; the relationship is Gemfile to Gemfile.lock. Init creates it once, with empty sections, and no hyperdrive command writes to it again.
Editing is one form of ownership; rejecting is another. The disabled: lists in that file permanently opt out any artifact you don’t want, per kind—a slash command that clashes with one of yours, say. Their mirror, enabled:, opts a gem in: add a gem’s name and its skills install even if the gem has no native Hyperdrive support, as long as it ships them in a skills/ directory. Bundle such a gem (its author may never have heard of Rails Hyperdrive) and hyperdrive prints a notice: it’s one line away from full adoption, its skills syncing and upgrading like everything else.
And deletions get the same respect as edits: remove a guideline’s line from .claude/hyperdrive/index.md, or the import line from CLAUDE.md itself, and no future sync adds it back.
#
Companion gems are ordinary gems on RubyGems, discoverable by a metadata declaration rather than a naming scheme or a separate registry. hyperdrive:discover asks the RubyGems search API for gems declaring the hyperdrive_targets metadata key, then intersects their declared targets with your lockfile:
$ bin/rails hyperdrive:discover
Found gems with rails-hyperdrive content for your stack:
✓ railties 8.1.3.1 → layered-rails-skills 3.1.0 (installed)
! railties 8.1.3.1 → require-profiler 0.3.1 — ships skill (suggested)
Run: bundle add require-profiler --group=development
Then: bin/rails hyperdrive:init
Each line reads left to right: a gem in your lockfile, and the companion that declares it as a target. The suggested line is the point: require-profiler, Vladimir Dementyev’s require-time profiler (announced on this blog), isn’t a dedicated companion at all, but a regular library gem that shipped a skill and declared hyperdrive_targets in its gemspec metadata. This declaration alone made it discoverable to every app running the command above, with no separate gem or registry involved.
#
Which brings us to you, gem authors: ship markdown, declare what it targets, and publish. That’s the whole contract, and the companion gem docs hold all of it. The minimal recipe is a skills/ directory plus one gemspec metadata key, hyperdrive_targets, the declaration that makes your gem discoverable. The hyperdrive.yml manifest is optional and exists only to declare deviations (gating, a custom directory layout, a command prefix).
There are three routes in:
- Your library gem ships a top-level
skills/directory itself (the way require-profiler does): one gem, one source of truth. - An existing skill repo packages itself as a gem (the way layered-rails-skills does ): standalone guidance (say, an architecture skill) not tied to any specific library.
- Anyone publishes a dedicated companion for a library that ships nothing (the way our toy sidekiq gem did): third-party guidance, no upstream buy-in needed.
And if two routes ever collide (like if a library grows its own skill after a third party already shipped one), same-named artifacts from different gems both install, each renamed with its source gem as a postfix, so no gem silently overwrites another.
SKILL.md follows the Agent Skills contract used by skills.sh and Claude Code—frontmatter with just name and description. A plain skills.sh skill installs with zero warnings and zero modifications, because all Rails-specific behavior lives outside the content, in the manifest.
And the compatibility runs both ways: your skill stays useful to people who never touch Rails Hyperdrive, through skills.sh or a plain git clone of your repo.
This parity extends to behaviors. layered-rails-skills doubles as a Claude Code plugin, and the one real difference between the two homes is command naming: /layered-rails:analyze as a plugin, /layered-rails-analyze as flat files in .claude/commands/. A single manifest line ( command_prefix: layered-rails) and one ERB helper ( canonical_render?) absorb that difference; everything else, cross-references between skills, agents, and commands included, survives the trip untouched.
All the gating power lives in the manifest. Here’s how layered-rails-skills gates its nine gem manuals:
gem: railties
skills:
layered-rails:
conditional:
references/gems/alba.md: { gem: "alba" }
references/gems/action-policy.md: { gem: "action_policy" }
references/gems/workflow.md: { gems: [workflow, workflow-activerecord] }
Per-file conditions are the start:
- Version requirements ride on the target they constrain:
gems: [sidekiq: ">= 7"]. - Multi-target gating covers both cases:
gems: [sidekiq, solid_queue]installs when either is bundled;gems: { all: [sidekiq, sidekiq-cron] }requires the whole set. - A manifest can fence on the engine itself:
hyperdrive_version: ">= 0.8"skips an artifact the running Rails Hyperdrive is too old to honor and prints an upgrade hint.
layered-rails-skills declares exactly that last fence, gem-wide: its commands are ERB templates, and an engine too old to render them should skip them rather than install them raw.
For the cases YAML can’t express, ERB templates render at install time against the app’s bundle, with exactly five helpers:
| Helper | Answers |
|---|---|
gem?("alba") |
is this gem in the bundle (optionally, at a version)? |
any_gem?("workflow", "workflow-activerecord") |
is at least one of these? |
all_gems?("sidekiq", "sidekiq-cron") |
are all of these? |
gem_version("railties") |
what version resolved? |
canonical_render? |
rendering the gem’s static copy, or installing into an app? |
Here’s a real example, from layered-rails-skills’ skill template—the gem-reference table trimming itself to your bundle:
<%- gem_refs = [
["action_policy", "Authorization framework", "action-policy.md", ["action_policy"]],
["view_component", "Component framework", "view-component.md", ["view_component"]],
[... 7 more rows ...]
].select { |row| any_gem?(*row[3]) } -%>
| Gem | Purpose | Reference |
|-----|---------|-----------|
<%- gem_refs.each do |label, purpose, file, _targets| -%>
| <%= label %> | <%= purpose %> | [<%= file %>](references/gems/<%= file %>) |
<%- end -%>
And because gating lives in the manifest, not the content, it adds nothing to the agent’s context—gate as extensively as you like.
A templated skill still ships a pre-rendered static copy for consumers who never run Rails Hyperdrive—skills.sh users, plain git clones. rake hyperdrive:skills:render regenerates that static copy from its ERB master, hyperdrive:skills:check fails CI when the two drift apart, and hyperdrive:manifest:check lints the manifest strictly, so a gating typo surfaces in your CI rather than as a warning in someone’s app.
If you maintain a Rails gem, this is the ask: your gem already ships code for machines and a README for humans. Shipping the agent-facing distillation of that README is an afternoon with the contract docs, and it makes your gem the one agents use correctly on the first try.
#
Honesty section, because the project is pre-1.0 so you should know where the edges are.
It speaks Claude Code today. The install targets .mcp.json, CLAUDE.md, and the .claude/ tree. The MCP server works with any MCP client, but content installation for other agents (AGENTS.md, Cursor rules) hasn’t shipped.
The contract is young, and breaking changes might be on the way. Pre-1.0 means pre-1.0: expect the contract to keep sharpening before it settles.
The ecosystem is a handful of gems. So far. Run hyperdrive:discover today and the list of suggestions is short. Growing it is exactly what the previous section is for.
It’s not a sandbox. The engine 403s outside development, checks request origins, and guards every tool—but as the security policy puts it, the read-only SQL gate exists to keep a confused AI from running DELETE FROM users by accident, not to enforce a privilege boundary. run_ruby is eval in your dev process, on purpose. The engine is development-only by design.
#
Yes, Rails Hyperdrive is a development-only Rails engine that gives AI coding agents an MCP server inside your dev process and installs agent knowledge from your gems. The difference in philosophy: Boost resolves your composer.json against a curated pack maintained inside Boost itself; Rails Hyperdrive ships zero content of its own—every skill and guideline arrives from the gems in your bundle, gated and versioned per artifact. Rails ships nothing like it today, though the idea has come up on the Rails forum.
bin/rails hyperdrive:init mounts an MCP server at /_hyperdrive in development and writes .mcp.json, which Claude Code picks up automatically. With bin/rails server running, the agent gets MCP tools answering from the booted process—routes, schema, logs, source lookup, and a read-only SQL gate among them.
Add a top-level skills/ directory and declare the hyperdrive_targets key in your gemspec metadata—that’s the minimal recipe, and it makes your gem discoverable to hyperdrive:discover. A hyperdrive.yml manifest is needed only when your content deviates from the defaults: gating, custom directories, a command prefix.
Nothing, by default: a plain hyperdrive:sync never overwrites a file you’ve modified. You pick the reconciliation explicitly—--merge (git three-way merge), --sidecar (the new version lands next to yours as .new), --resolve (each .new is handed to an agent command you configure, git mergetool style), or --overwrite (the only one that loses work).
#
Here’s our prediction: within a couple of years, “does it ship agent skills?” will join “does it have docs?” and “is it maintained?” on the checklist every Rails developer runs before adding a gem. Agents already write a lot of the code that calls your library; a gem that explains itself to agents gets used correctly, recommended by the tools themselves, and kept in the Gemfile. One that doesn’t collects hallucinated APIs and frustrated issue reports.
Rails spent twenty years proving that strong defaults and substitutions can coexist. Agent knowledge deserves the same deal: a contract any gem can join.
Evil Martians has shipped open source the Rails community runs in production for over a decade (AnyCable and TestProf among them) and Rails Hyperdrive is our wager on what a gem ships next, an that explanation agents can act on.
The whole contract fits in one doc page. Bring your gem!