rails-ai-context 5.36.7 → 5.36.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +220 -0
  3. data/README.md +6 -0
  4. data/docs/CLI.md +1 -1
  5. data/docs/COMPATIBILITY.md +19 -0
  6. data/docs/CONFIGURATION.md +1 -1
  7. data/docs/GUIDE.md +1 -1
  8. data/docs/PROMPTS.md +3 -3
  9. data/docs/SECURITY.md +37 -2
  10. data/docs/SETUP.md +191 -0
  11. data/docs/STANDALONE.md +2 -2
  12. data/docs/TOOLS.md +5 -5
  13. data/docs/TROUBLESHOOTING.md +6 -2
  14. data/exe/rails-ai-context +53 -20
  15. data/lib/generators/rails_ai_context/install/install_generator.rb +8 -4
  16. data/lib/rails_ai_context/boot_manager.rb +7 -1
  17. data/lib/rails_ai_context/claude_code_settings.rb +1006 -0
  18. data/lib/rails_ai_context/cli/app_root.rb +126 -16
  19. data/lib/rails_ai_context/cli/entry_boot.rb +60 -6
  20. data/lib/rails_ai_context/configuration.rb +116 -34
  21. data/lib/rails_ai_context/doctor.rb +482 -24
  22. data/lib/rails_ai_context/gem_lock.rb +14 -7
  23. data/lib/rails_ai_context/initializer_settings.rb +603 -0
  24. data/lib/rails_ai_context/install/claude_plugin_removal.rb +107 -0
  25. data/lib/rails_ai_context/install/program.rb +133 -6
  26. data/lib/rails_ai_context/install/selection_record.rb +11 -5
  27. data/lib/rails_ai_context/introspectors/env_references.rb +1 -1
  28. data/lib/rails_ai_context/introspectors/gemfile_gems.rb +2 -2
  29. data/lib/rails_ai_context/mcp_config_generator.rb +18 -6
  30. data/lib/rails_ai_context/redaction.rb +749 -26
  31. data/lib/rails_ai_context/resources.rb +15 -0
  32. data/lib/rails_ai_context/safe_path.rb +101 -11
  33. data/lib/rails_ai_context/secret_columns.rb +1 -1
  34. data/lib/rails_ai_context/server.rb +6 -1
  35. data/lib/rails_ai_context/sql_gate.rb +5 -0
  36. data/lib/rails_ai_context/tools/get_env.rb +20 -30
  37. data/lib/rails_ai_context/tools/get_partial_interface.rb +12 -3
  38. data/lib/rails_ai_context/tools/get_view.rb +6 -3
  39. data/lib/rails_ai_context/tools/review_changes.rb +74 -29
  40. data/lib/rails_ai_context/tools/safe_call.rb +5 -0
  41. data/lib/rails_ai_context/tools/search_code.rb +24 -8
  42. data/lib/rails_ai_context/tools/search_docs.rb +2 -1
  43. data/lib/rails_ai_context/update_check.rb +17 -4
  44. data/lib/rails_ai_context/version.rb +1 -1
  45. data/lib/rails_ai_context.rb +26 -0
  46. metadata +4 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8498e25402e035ed53ab2c0dff9dff31da4c849e616632069891de80f38ff7f4
4
- data.tar.gz: 92dac2339bbe11cc0486ad449f8b84fd5fe840d15c31e56414b53b9b0d7e9fbc
3
+ metadata.gz: e4f5530fd8b0a92932d1810f018d01de51af456064f92de2f9b95749360e43a5
4
+ data.tar.gz: dd8098d2f6d049088e92d857a407a69e84a19963379eaa698749bab6d5564539
5
5
  SHA512:
6
- metadata.gz: d06dccf666aa9a2ca94e8a42f5191a9d7cf98f2a8f9799f6ab0afa941c6d4e397ecefe5aa71979bb91d6f667151345b161795bd81058a4e4810d5f29828e64eb
7
- data.tar.gz: 324d72633467fc133a608d848fa3dce1f3f21cc600ab0a9d637288a5ba05f1f69c356aaf3a2871a5217a5c4498787a279695554d8c08ff80af303ba1a8857984
6
+ metadata.gz: 5ac4f8fb6bdd15d56bf3db2aec1b5e53d3ca39f437d2acead40769014f3747546a931c2ad531250c614ff3f418eaae5d82bf84fe2790036e7931c50f515dd12b
7
+ data.tar.gz: 943060825f72f4e118bebc8160a209787582d8223050ca65cbdd6149bff4819fc12966962dc2ceb1f999689571db1121f908b68bb4ca27563094d7cf1874b6fd
data/CHANGELOG.md CHANGED
@@ -5,6 +5,226 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [5.36.9] - 2026-10-23
9
+
10
+ ### Security
11
+
12
+ - **`rails_read_logs` and `rails_query` hide a value under a plural secret name
13
+ or a list of codes** (#606). A secret word ending a name in its plural (`api_keys`,
14
+ `tokens`, `secrets`, `passwords`, `accessTokens`) named no secret, and
15
+ neither did `recovery_codes`, `backup_codes`, `recoveryCodes` or
16
+ `backupCodes`, so their values came back from a log line, inline or bound,
17
+ and `rails_query` returned such a column. A list or hash logged under a
18
+ secret name is now filtered whole, on one line or spread over lines, where
19
+ only its first element was, and so are a query parameter with a secret word
20
+ anywhere in its bracketed name (`user[password][new]=`) and every value of
21
+ `API_KEYS=a,b`. In a log line a count stays shown (`tokens_used`,
22
+ `token_count`, `prompt_tokens: 12`, `inputTokens: 12`), and so do `keys`,
23
+ `primary_keys` and `cache_keys`; `rails_query` still refuses a column whose
24
+ name holds `token` unless `query_allowed_columns` lists it.
25
+
26
+ ### Added
27
+
28
+ - **The repository is a Claude Code plugin marketplace.** `claude plugin install
29
+ rails-ai-context --marketplace crisnahine/rails-ai-context --scope project`
30
+ adds the MCP server and gives each session in a Rails app the six rules the
31
+ context files open with (unless the app's own files carry them or its config
32
+ turns them off), and writes no context files or MCP config. It runs the
33
+ installed rails-ai-context, for the `ruby` on `PATH`, Windows included.
34
+ Where it cannot serve (no gem for that Ruby, an older one, a Ruby too old,
35
+ several apps or none) its server still connects, with no tools, and tells
36
+ the session why, naming the Ruby and the folder it ran in, since Claude Code
37
+ skips a plugin server that failed to start in every new session for 15
38
+ minutes. Its command line names no versioned folder, so one
39
+ `allowedMcpServers` entry admits it across updates. See
40
+ [SETUP.md](docs/SETUP.md#as-a-plugin).
41
+ - **`doctor` knows the plugin.** It reads Claude Code's settings, install
42
+ registry, `/mcp` toggles and MCP policy per session folder, as Claude Code
43
+ does. Where the plugin serves the app it counts as Claude Code's MCP config;
44
+ where it does not run, `doctor` says why (not installed, a local marketplace
45
+ folder that is gone, turned off in `/mcp`, blocked by the MCP or marketplace
46
+ policy, naming each rule and the file that holds it, or a managed settings
47
+ file Claude Code cannot read) instead of sending you to `init`. A
48
+ `.mcp.json`, user scope or local scope server of the gem's, an HTTP one
49
+ included, that a session would start beside it is a warning naming the
50
+ commands that remove it, since Claude Code would list every tool twice. Where
51
+ Claude Code never ran, as on CI, what the plugin needs is a warning, so a
52
+ STRICT run stays green. Where an organization deploys `managed-mcp.json`,
53
+ which starts its own servers alone, `doctor` judges Claude Code by it; it
54
+ follows a `CLAUDE_CONFIG_DIR` set in the settings' `env` block, and warns
55
+ about one there or in the environment that is not an absolute path, which no
56
+ one can read from outside a session.
57
+
58
+ ### Changed
59
+
60
+ - **The install writes no Claude Code server where the app's committed settings
61
+ turn the plugin on.** `init`, the install generator and `context` leave
62
+ `.mcp.json` alone there and say why, so doctor's fix to remove an old entry
63
+ lasts; `init` in a folder holding one app does the same where that folder's
64
+ committed settings turn the plugin on, and writes the app's own `.mcp.json`
65
+ for sessions started in the app. Its summary marks the plugin off where it
66
+ does not start for you. Where only your own settings turn the plugin on,
67
+ Claude Code's next step says to turn the `.mcp.json` server off with `/mcp`.
68
+ - **The update notice in a plugin's server says to reconnect it.** It named
69
+ `init`, which would add a second server beside the plugin's.
70
+ - **`uninstall` names the Claude Code plugin** that runs the installed gem, and
71
+ the commands that turn it off for the app (one per folder whose local
72
+ settings its sessions read) and take it out, before it suggests removing the
73
+ gem.
74
+
75
+ ### Fixed
76
+
77
+ - **A YAML alias in `.rails-ai-context.yml` no longer stops the app's boot.**
78
+ The loader refused aliases and the error escaped the engine's initializer.
79
+ Aliases now apply, and YAML that will not load warns and keeps the defaults.
80
+ So does a value of the wrong shape, such as a nested list for `ai_tools`, a
81
+ list for `preset` or a mapping for a number, which raised past the
82
+ initializer too, in one short line however many aliases the file nests. A
83
+ number written as text reads as that number: `1e5` had read as 1, a
84
+ `query_timeout` of `010` as 8, and a quoted number failed the tools that
85
+ compare it.
86
+ - **`rails-ai-context init` and `context` read an odd config file as the
87
+ loader does** (#520). A file holding a list, not settings, and a `tool_mode`
88
+ that is not a name stopped both with a Ruby error. They now warn and keep the
89
+ default, as the loader does.
90
+ - **A number setting from a configure block is checked when it is set.** Text
91
+ such as an ENV lookup, a negative count or a list was stored as given for the
92
+ settings with no writer of their own, and a tool failed on it later. It now
93
+ reads as the number it is written as, and a wrong value raises ArgumentError
94
+ at boot, as `cache_ttl` already did; from the YAML file it warns and keeps
95
+ the default. `live_reload_debounce` likewise takes a finite number of
96
+ seconds, zero or more.
97
+ - **A query log tag in a statement's trailing comment is read as a tag.**
98
+ `rails_read_logs` read the comment as SQL, so where the statement named a
99
+ secret column, the first tag's value (`/*application='App'*/`) was
100
+ filtered. A tag is now filtered only when its key names a secret, in the
101
+ `key='value'` and `key:value` forms; MySQL's `/*!` and `/*+` stay SQL.
102
+ - **`live_reload: "false"` in `.rails-ai-context.yml` no longer turns live
103
+ reload on.** Any name was taken and meant auto. `true` and `false`, as
104
+ booleans or text, and `auto` now count; anything else warns and keeps auto
105
+ from the file, and raises ArgumentError from a configure block.
106
+ - **A Ruby file the app cannot read no longer stops the static tier.** Since
107
+ 5.36.8 the static tier reads the security settings an app's Ruby config
108
+ writes, and one file it could not open made every answer an error; no boot
109
+ loads that file either, so it now names no setting.
110
+ - **A `BUNDLE_GEMFILE` inherited from another project is named.** The app
111
+ booted against that bundle and failed with a bare LoadError; the warning
112
+ bundle exec already had now covers the variable on its own.
113
+ - **A running server survives `gem cleanup`.** The gem loads its files as it
114
+ needs them, so a tool call after its copy was removed raised a LoadError that
115
+ ended the server. A call that needs a file it has not loaded yet now answers
116
+ with an error that says to reconnect the server, or over HTTP to restart the
117
+ process serving it.
118
+ - **A boot failure names the Ruby the app pins** when another one runs and the
119
+ failure can come from that, a missing gem or a load error, as when a client
120
+ starts in a monorepo's root that pins its own.
121
+ - **No update notice where the app's lockfile names the version running.**
122
+ That lockfile decides the copy that serves the app, so `gem update` changed
123
+ nothing there.
124
+ - **A lockfile rewritten within the clock's resolution is read again.** The
125
+ app's bundle was cached by modification time alone, so a second write in the
126
+ same tick kept the first answer.
127
+ - **An app whose bundle has its own path runs its own copy.** Where `bundle
128
+ config set path`, a deployment bundle or `BUNDLE_PATH` put the gem under
129
+ the app, the installed copy served it and the app's boot loaded the
130
+ bundle's copy over it, two copies in one process, whichever version the
131
+ bundle locked. The bundle's path is now read as Bundler reads its settings.
132
+
133
+ ## [5.36.8] - 2026-10-23
134
+
135
+ ### Security
136
+
137
+ - **The static tier keeps the security settings the app's Ruby config writes.**
138
+ No initializer runs without a boot, so on the static tier (`--no-boot`, or a
139
+ boot that failed) a `sensitive_patterns`, `excluded_paths` or `skip_tools`
140
+ value set in a `RailsAiContext.configure` block was dropped, and a file the
141
+ block made sensitive was searched and read. The static tier now reads every
142
+ Ruby file of the app that names the gem, without running it: a file under
143
+ `config/`, and the top level of any other, whole, and a method's body
144
+ elsewhere (a tool's, a helper's) for what it writes, with tests, installed
145
+ gems and generator templates left out. The literal values those files write
146
+ to the three keys are added to the YAML's; a removal is left out, the
147
+ stricter reading. A value it cannot read, such as one built from a constant
148
+ or the environment, a pattern changed in place, or the configuration reached
149
+ through `send`, stops the static tier with an error that names the file and
150
+ line.
151
+ - **A sensitive pattern covers its file wherever it sits.**
152
+ `config/database.yml` and the other patterns with a `/` matched only at the
153
+ app root, so an engine's, a pack's or a test/dummy app's copy was read by
154
+ every file tool, and `**/id_rsa` missed an `id_rsa` at the root. A pattern
155
+ now reads the way a `.gitignore` line does: one with a `/` names its file
156
+ below the root too, a leading `/` anchors it at the root, a trailing `/`
157
+ names a folder and everything in it, and `**/` stands for any number of
158
+ folders, none included.
159
+ A file a link brings in is also asked by its name in the app's repository, so
160
+ a link to a pack's `config/` folder cannot hide the `config/` a pattern looks
161
+ for. `doctor`'s gitignore check reads the files below the root the same way.
162
+ A path a caller names that is longer than 4096 bytes, which no file system
163
+ holds, is answered as not found before any pattern reads it.
164
+ - **`rails_search_code` reads a path the way the file system does.** A path
165
+ with a `.` segment (`./`, `config/.`) made ripgrep print `./config/...`, and a
166
+ link inside the app to `config/` made it print the link's name, so no
167
+ sensitive pattern matched and a sensitive file's lines came back. The path is
168
+ now cleaned, and one inside the app is searched as the folder it leads to.
169
+ - **`rails_search_code` and `rails_review_changes` keep a private key's body
170
+ hidden on every line they show.** A search row is redacted with its whole
171
+ file, but ripgrep searched files over `max_file_size`, which the redaction
172
+ does not read, so each body line of a PEM key in such a file came back on its
173
+ own, unfiltered. ripgrep now skips those files as the Ruby search does, and a
174
+ file named as the path over the limit answers "File too large". A diff hunk
175
+ that opened inside a key showed its body lines the same way; each diff line
176
+ is now redacted with the whole side of the file it comes from, the hunk
177
+ header's line too. The diff is read with the repository's colour, external
178
+ diff and textconv settings off, and a diff over `max_file_size` or a merge
179
+ conflict's combined diff shows no lines. A submodule's change shows as the
180
+ commits it moves between, where `diff.submodule=diff` printed its files'
181
+ lines unredacted. A changed file that is not UTF-8 is read, where the tool
182
+ stopped with an encoding error.
183
+ - **`rails_read_logs` filters a secret column's value written inline in a
184
+ statement.** MySQL and MariaDB run no prepared statements, so Active Record
185
+ logs every value inline with backquoted names, and `insert_all` and
186
+ `upsert_all` write their rows inline on every adapter. Password hashes,
187
+ tokens and reset keys came back from INSERT, UPDATE and WHERE lines, from the
188
+ `CAST()` and `LOWER()` a uniqueness validation writes, and from the database
189
+ errors that repeat a row's values. A value compared with a secret column
190
+ (through a JSON path too), listed for it with `IN` or `BETWEEN`, inserted
191
+ under it, or bound to its placeholder is now filtered whatever quotes the
192
+ names and the value carry, and so is a value whose column cannot be told in a
193
+ statement that names a secret column, every value of an `INSERT` with no
194
+ column list, and a secret-named key in JSON a value holds. PostgreSQL's
195
+ failing row is filtered whole, as it names no column, MySQL's duplicate entry
196
+ unless Rails named the index after columns that hold no secret, PostgreSQL's
197
+ duplicate key when the key names a secret, and the statement excerpt a MySQL
198
+ syntax error quotes; a message cut off is filtered to the end of its line. A
199
+ JSON log line's strings are read the same way, a value under a secret's key
200
+ and a JSON logger's binds included.
201
+ - **`rails_get_config(section: "env")` hides a DSN or a webhook URL wherever it
202
+ is set.** A Dockerfile `ENV` or `ARG` value, an `.env.example` value and a
203
+ default in code went through the generic redaction only, which reads a URL's
204
+ password but not a key before `@` or a token in the path, so a Sentry DSN or
205
+ a Slack webhook printed in full where the Kamal section hid the same value.
206
+ Those values now read `[FILTERED]` when their shape can carry a secret: a URL
207
+ whose user part is a key or a password, a webhook's address, a token in a
208
+ query, a key-like run of letters and digits, and a `BUNDLE_<HOST>` gem server
209
+ credential. Paths, hosts and local or public URLs still show. An
210
+ `.env.example` value in quotes keeps a `#`, where the part after it printed
211
+ as a comment, and an inline comment is hidden when it holds a secret's shape.
212
+ - **`rails_get_view` and `rails_get_partial_interface` match a path as plain
213
+ text.** A missing view's folder and a partial's name were globbed as written,
214
+ so each `{a,b}` group doubled the work for every view file, and one crafted
215
+ path held a server thread for minutes.
216
+ - **`rails_query` refuses a `PROCEDURE` clause on MySQL and MariaDB.**
217
+ MariaDB's `SELECT ... PROCEDURE ANALYSE()` returns each column's lowest and
218
+ highest values, lengths and null counts, a secret column's included, under
219
+ names of its own that the result redaction does not read. `SHOW PROCEDURE
220
+ STATUS` still runs.
221
+ - **The patterns that find a URL's password, a JWT and an `sk-` key read a
222
+ line in time that grows with its length.** The URL pattern tried a scheme from
223
+ each letter of a run, and the JWT and `sk-` patterns read to a run's end from
224
+ every repeat in it, so a 100 KB log line or env value took 15 to 35 seconds. A
225
+ URL's password is now read from its `://`, after a shell default's `:-` too,
226
+ and a JWT and an `sk-` key are read so that a run of repeats costs its length.
227
+
8
228
  ## [5.36.7] - 2026-10-22
9
229
 
10
230
  ### Security
data/README.md CHANGED
@@ -81,6 +81,12 @@ gem install rails-ai-context
81
81
  rails-ai-context init # or: rails-ai-context init --tools claude --defaults
82
82
  ```
83
83
 
84
+ As a Claude Code plugin, which adds the MCP server and gives each session the six rules below, and writes no context files or MCP config. It runs the installed gem, so install that first, with `gem install rails-ai-context` (or `bundle add` alone, when the bundle installs where your `ruby` looks). Run it in the app's folder, and commit the `.claude/settings.json` it writes; the install generator and `init` then leave Claude Code's `.mcp.json` to the plugin ([details](docs/SETUP.md#as-a-plugin)):
85
+
86
+ ```bash
87
+ claude plugin install rails-ai-context --marketplace crisnahine/rails-ai-context --scope project
88
+ ```
89
+
84
90
  `--tools` takes `claude`, `cursor`, `copilot`, `opencode`, `codex` or `all`. Add `--cli` for context files only, or `--mcp-only` for the MCP config only. Re-running is safe: with `--defaults` it keeps your recorded tools and setup, keeps what you have and adds what is missing, and in a `CLAUDE.md` you wrote it only touches the block between its `<!-- BEGIN/END rails-ai-context -->` markers. Every step is in [What the install generator does](docs/GUIDE.md#what-the-install-generator-does).
85
91
 
86
92
  Needs Ruby 3.1+ and Rails 7.0+, on Linux, macOS or Windows. Optional: `brakeman` for `security_scan`, `listen` for `watch`, `ripgrep` for faster `search_code`.
data/docs/CLI.md CHANGED
@@ -320,7 +320,7 @@ rails-ai-context uninstall
320
320
  bundle exec rails-ai-context uninstall # in-Gemfile; then bundle remove rails-ai-context
321
321
  ```
322
322
 
323
- What you wrote stays: your lines around the markers in a shared `CLAUDE.md` or `AGENTS.md`, your rule files, other servers in an MCP config, and a pre-commit hook you changed by hand (named, not touched). Its own config files go even when you edited them: `.rails-ai-context.yml` and the initializer configure nothing once the gem is gone. An HTTP entry under the gem's name goes too, since only the gem's endpoint answers it. A `mount RailsAiContext::Engine` line in `config/routes.rb` is named with its line number for you to delete. In a folder of apps it uninstalls every app below and the folder's MCP configs. It needs no boot. An entry you added by hand to another client's own config from `mcp-entry` (Claude Desktop and the like) lives outside the app, so remove it there. Its last lines say how to remove the gem itself: `bundle remove rails-ai-context` when the Gemfile lists it, `gem uninstall rails-ai-context` once no other app uses the installed copy. Details in the [Guide](GUIDE.md#uninstalling).
323
+ What you wrote stays: your lines around the markers in a shared `CLAUDE.md` or `AGENTS.md`, your rule files, other servers in an MCP config, and a pre-commit hook you changed by hand (named, not touched). Its own config files go even when you edited them: `.rails-ai-context.yml` and the initializer configure nothing once the gem is gone. An HTTP entry under the gem's name goes too, since only the gem's endpoint answers it. A `mount RailsAiContext::Engine` line in `config/routes.rb` is named with its line number for you to delete. In a folder of apps it uninstalls every app below and the folder's MCP configs. It needs no boot. An entry you added by hand to another client's own config from `mcp-entry` (Claude Desktop and the like) lives outside the app, so remove it there. Where the Claude Code plugin runs the gem, it names the commands that turn the plugin off and take it out ([SETUP](SETUP.md#as-a-plugin)), since the plugin's server fails once the gem is gone. Its last lines say how to remove the gem itself: `bundle remove rails-ai-context` when the Gemfile lists it, `gem uninstall rails-ai-context` once no other app uses the installed copy. Details in the [Guide](GUIDE.md#uninstalling).
324
324
 
325
325
  ### `mcp-entry`
326
326
 
@@ -366,6 +366,25 @@ Proof sources:
366
366
  security, gate, redaction and database specs on Rails 7.0 (Ruby 3.1), 7.1,
367
367
  7.2 and 8.1 against live PostgreSQL; and the unit suite and the E2E harness
368
368
  on Linux, macOS and Windows.
369
+ In the v5.36.8 release QA: each fix's example failing on the code before
370
+ it; `rails_read_logs` against Active Record 8.0's own logs from live
371
+ PostgreSQL 16, MySQL 8.4 and MariaDB 13 for inline values, binds and the
372
+ errors that repeat a row, and against a JSON logger's lines; the static-tier
373
+ settings reader, `rails_search_code`, `rails_get_config` and
374
+ `rails_review_changes` on five open-source apps and two private ones with
375
+ `--no-boot`, compared with v5.36.7 and every changed answer checked; the
376
+ touched specs on Rails 7.0 (Ruby 3.1) and on prism 1.4; and the unit suite
377
+ and the E2E harness on Linux, macOS and Windows.
378
+ In the v5.36.9 release QA: each fix's example failing on the code before
379
+ it; `doctor`'s plugin checks against what Claude Code 2.1.308 and 2.1.309
380
+ do with the same settings (marketplaces, enabled plugins, server command
381
+ and URL rules, managed files and `managed-mcp.json`), and the plugin's
382
+ launcher under Ruby 2.6 and 3.1; `rails_read_logs` and `rails_query` on
383
+ plural secret names and code lists, compared with v5.36.8 in a booted
384
+ SQLite app; the QA ladder on the SQLite labs in every tier, the E2E harness
385
+ and one large open-source app, booted; the unit suite on Ruby 3.4 and on
386
+ Rails 7.0 (Ruby 3.1); and the unit suite and the E2E harness on Linux,
387
+ macOS and Windows.
369
388
  2. Non-crash coverage for every built-in tool including `get_view` in
370
389
  `spec/e2e/in_gemfile_install_spec.rb`'s full-tool sweep; output correctness
371
390
  (ivar cross-check, render-form detection, partial interfaces) verified
@@ -39,7 +39,7 @@ preset: full
39
39
  ### Precedence
40
40
 
41
41
  > [!IMPORTANT]
42
- > `configure` block > YAML > Defaults, merged key by key. The file is read once, at boot, before `config/initializers`, so an initializer may assign a key or edit it in place (`config.skip_tools << "rails_query"`) and both survive. A block that runs *before* the file - in `config/application.rb` or an environment file - must assign a key to keep it (`config.skip_tools = ["rails_query"]`), because an in-place edit there is replaced when the file loads. A key no block assigns keeps the YAML value. Corrupted YAML degrades gracefully with a warning, and a key the gem does not know warns on stderr and is ignored while the rest of the file applies.
42
+ > `configure` block > YAML > Defaults, merged key by key. The file is read once, at boot, before `config/initializers`, so an initializer may assign a key or edit it in place (`config.skip_tools << "rails_query"`) and both survive. A block that runs *before* the file - in `config/application.rb` or an environment file - must assign a key to keep it (`config.skip_tools = ["rails_query"]`), because an in-place edit there is replaced when the file loads. A key no block assigns keeps the YAML value. Corrupted YAML degrades gracefully with a warning, and a key the gem does not know warns on stderr and is ignored while the rest of the file applies. On the static tier no initializer runs: the literal values that the app's Ruby files naming the gem write to `sensitive_patterns`, `excluded_paths` and `skip_tools` are read without running them and added to the YAML's, and one the gem cannot read stops the static tier with an error naming its file and line ([SECURITY.md](SECURITY.md#configuration)).
43
43
 
44
44
  ---
45
45
 
data/docs/GUIDE.md CHANGED
@@ -159,7 +159,7 @@ rails-ai-context uninstall # standalone; then, once no app uses it
159
159
  - The lines the install added to `.gitignore`, with their comments.
160
160
  - The pre-commit hook, when it is the one the install wrote and covers only this app. A hook that covers other apps of the repository loses this one; a hook changed by hand is named and left for you.
161
161
 
162
- A `mount RailsAiContext::Engine` line in `config/routes.rb` is your code, so it is named with its line number, not edited. Run in a folder of apps, it does the same for every app below and for the folder's MCP configs. It needs no boot, so it works on an app that cannot start, and the binary of any install can run it: in an app whose bundle pins a version from before `uninstall` existed, `gem install rails-ai-context` and run `rails-ai-context uninstall`.
162
+ A `mount RailsAiContext::Engine` line in `config/routes.rb` is your code, so it is named with its line number, not edited. Run in a folder of apps, it does the same for every app below and for the folder's MCP configs. It needs no boot, so it works on an app that cannot start, and the binary of any install can run it: in an app whose bundle pins a version from before `uninstall` existed, `gem install rails-ai-context` and run `rails-ai-context uninstall`. Where the Claude Code plugin runs the gem, it first names the commands that turn the plugin off and take it out ([SETUP](SETUP.md#as-a-plugin)); a release from before the plugin names none, so turn it off yourself with `claude plugin disable rails-ai-context@rails-ai-context --scope local`.
163
163
 
164
164
  ---
165
165
 
data/docs/PROMPTS.md CHANGED
@@ -276,8 +276,8 @@ rails-ai-context isn't working right in this Rails app. Find out why and fix it.
276
276
  1. Check the version: `bundle exec rails-ai-context version` if the Gemfile lists rails-ai-context, else `rails-ai-context version`. If it is older than 5.35.0, upgrade first (`bundle update rails-ai-context --conservative`, or `gem install rails-ai-context -v '>= 5.35.0'`) and check the version again: older versions read `--defaults` in step 5 as every AI tool. If the Gemfile pins it below 5.35.0, change its rails-ai-context line to `gem "rails-ai-context", ">= 5.35.0", group: :development` and run the update again.
277
277
  2. Run doctor: `bundle exec rails ai:doctor` if the Gemfile lists the gem, else `rails-ai-context doctor`. Read every failing or warning check and do what each one says.
278
278
  3. If doctor itself cannot run because the app won't boot, run `bundle exec rails-ai-context tool onboard --no-boot` (or without bundle exec when standalone) to confirm the tools still answer from source, and tell me what stops the boot.
279
- 4. If my MCP tools are missing, open the MCP config file of the AI tool you are (Claude Code .mcp.json, Cursor .cursor/mcp.json, GitHub Copilot .vscode/mcp.json, OpenCode opencode.json, Codex CLI .codex/config.toml) and check it has a rails-ai-context entry. Run that entry's command and args by hand with `--help` added at the end, for example `bundle exec rails-ai-context serve --help`, and check it prints the serve usage.
280
- 5. Repair the install by running it again: `bundle exec rails generate rails_ai_context:install --defaults` if the Gemfile lists the gem, otherwise `rails-ai-context init --defaults`. On a re-run `--defaults` keeps the recorded AI tools and setup, keeps what exists and adds what is missing. For Codex CLI after a Ruby version change, it also refreshes the Ruby paths saved in .codex/config.toml.
279
+ 4. If my MCP tools are missing, open the MCP config file of the AI tool you are (Claude Code .mcp.json, Cursor .cursor/mcp.json, GitHub Copilot .vscode/mcp.json, OpenCode opencode.json, Codex CLI .codex/config.toml) and check it has a rails-ai-context entry. Run that entry's command and args by hand with `--help` added at the end, for example `bundle exec rails-ai-context serve --help`, and check it prints the serve usage. With the Claude Code plugin (rails-ai-context@rails-ai-context enabled in .claude/settings.json, .claude/settings.local.json or my user settings, or doctor names the plugin) there is no entry: where its server has no tools, its instructions in your context say why and what to run; where /mcp lists it as failed, ask me to start `claude --debug` and read that server's stderr. `ruby -S gem list rails-ai-context` run here shows the versions the `ruby` Claude Code starts has.
280
+ 5. If doctor named a missing or broken MCP config, missing context files, or Codex's saved Ruby paths, repair the install by running it again: `bundle exec rails generate rails_ai_context:install --defaults` if the Gemfile lists the gem, otherwise `rails-ai-context init --defaults`. On a re-run `--defaults` keeps the recorded AI tools and setup, keeps what exists and adds what is missing. For Codex CLI after a Ruby version change, it also refreshes the Ruby paths saved in .codex/config.toml.
281
281
  6. Tell me what was wrong, what you changed, and the "Next step" lines the install printed if I need to do one.
282
282
  ```
283
283
 
@@ -288,7 +288,7 @@ Removes everything the gem wrote and nothing you wrote: your part of a shared `C
288
288
  ```text
289
289
  Remove rails-ai-context from this Rails app completely. If the rails-ai-context entry in my MCP config runs `docker` (this app's Ruby lives in a container), run steps 1 and 3 inside that container, prefixed the way the entry's args are.
290
290
 
291
- 1. Run `bundle exec rails-ai-context uninstall` if the Gemfile lists rails-ai-context, else `rails-ai-context uninstall`. If the command is missing or unknown, run `gem install rails-ai-context -v '>= 5.35.0'` and then `rails-ai-context uninstall`.
291
+ 1. Run `bundle exec rails-ai-context uninstall` if the Gemfile lists rails-ai-context, else `rails-ai-context uninstall`. If the command is missing or unknown, run `gem install rails-ai-context -v '>= 5.35.0'` and then `rails-ai-context uninstall`. If its output names the Claude Code plugin, run the commands it names for it; if it does not, but rails-ai-context@rails-ai-context is enabled in .claude/settings.json, .claude/settings.local.json or my user settings, run `claude plugin disable rails-ai-context@rails-ai-context --scope local` here.
292
292
  2. If its output names a config/routes.rb line that mounts RailsAiContext::Engine, delete that line.
293
293
  3. If the Gemfile lists rails-ai-context, run `bundle remove rails-ai-context`.
294
294
  4. If .git/info/exclude lists rails-ai-context files (a private setup), delete those lines. If I added a rails-ai-context entry to another MCP client's own config (Claude Desktop, Windsurf, Zed and the like), remove that entry there too.
data/docs/SECURITY.md CHANGED
@@ -57,6 +57,7 @@ Before any query reaches the database, the SQL gate (`RailsAiContext::SqlGate`)
57
57
  - **Blocks lock clauses**: FOR UPDATE, FOR SHARE, FOR NO KEY UPDATE, FOR KEY SHARE and LOCK IN SHARE MODE, with whatever follows them (NOWAIT, SKIP LOCKED, OF, WAIT). On PostgreSQL the clause is read on quote-masked text, so `LIKE '%ready for update%'` is data: its read-only transaction refuses a locking read anyway. MySQL's read-only transaction lets `FOR SHARE` and `LOCK IN SHARE MODE` through, so there and on SQLite the raw text decides
58
58
  - **Blocks SHOW forms that tell of other sessions, replication and logs, on MySQL and MariaDB**: `SHOW GRANTS`, `SHOW PROCESSLIST` and `SHOW FULL PROCESSLIST`, `SHOW BINLOG`, `SHOW BINARY LOGS` and `SHOW BINARY LOG STATUS`, `SHOW RELAYLOG EVENTS`, the `SHOW SLAVE` and `SHOW SLAVES` forms, `SHOW MASTER`, the `SHOW REPLICA` and `SHOW REPLICAS` forms, `SHOW ALL SLAVES STATUS` and `SHOW ALL REPLICAS STATUS`, `SHOW ENGINE ... STATUS`, `SHOW PROFILE` and `SHOW PROFILES`, `SHOW OPEN TABLES`, `SHOW EXPLAIN FOR` and `SHOW ANALYZE FOR`, `SHOW USER_STATISTICS`, `SHOW CLIENT_STATISTICS` and the `SHOW WSREP_` forms. The answer is "Blocked: sensitive SHOW command". They are matched on the raw text, so the words inside a string literal are refused too
59
59
  - **Blocks `EXPLAIN FOR CONNECTION`, on MySQL and MariaDB**: it shows the plan of the statement another session is running. It is refused also as `DESCRIBE` or `DESC`, with a `FORMAT`, and inside a version comment. The answer is "Blocked: EXPLAIN FOR CONNECTION is not run. It shows the plan of the statement another session is running." It is read on quote-masked text, so a string literal that holds the words runs
60
+ - **Blocks a `PROCEDURE` clause, on MySQL and MariaDB**: MariaDB's `SELECT ... PROCEDURE ANALYSE()` returns each column's lowest and highest values, a secret column's too, under names of its own that the result redaction does not read. `PROCEDURE` is a reserved word there, so a bare one outside a `SHOW` statement is refused in any spelling, and `SHOW PROCEDURE STATUS` runs. The answer starts "Blocked: a PROCEDURE clause is not run."
60
61
  - **Blocks SHOW with a WHERE clause**, on every family. MySQL's statement timeout holds `SELECT` only, so a `SHOW ... WHERE` filter would run past `query_timeout`. `SHOW ... LIKE` runs. The answer names the query to use: "Blocked: SHOW with a WHERE clause is not run, because MySQL's statement timeout does not hold it. SHOW ... LIKE runs. For any other filter, SELECT from information_schema, for example `SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE()`."
61
62
  - **Blocks SELECT INTO**: prevents table creation via SELECT. After a `WITH` clause an `INTO` can belong to a write, and such a statement is answered as that write: `WITH ... INSERT INTO ...` answers "Blocked: contains INSERT", and `REPLACE` and `MERGE` are named the same way. `INTO OUTFILE` / `INTO DUMPFILE` disk writes, and the file and network functions below, stay matched on the **raw (unmasked) text**. No read-only transaction stops a server-side file write, so `INTO OUTFILE` inside a string literal is refused on every database family, and a file function inside one on the family that has it
62
63
  - **Blocks file and network functions**, each on the database family that has it. PostgreSQL: `pg_read_file`, `pg_read_binary_file`, `pg_ls_dir`, `pg_stat_file`, `lo_import`, `lo_export`, `dblink*` and their relatives. MySQL and MariaDB: `LOAD DATA` and `LOAD_FILE`. SQLite: `load_extension`. They are checked before and after comment stripping
@@ -357,6 +358,16 @@ A placeholder whose name ends in `.example`, `.sample`, `.template` or
357
358
  characters, still does, and so does a path pattern (one with a `/`, such as
358
359
  `.ssh/*`), which covers everything under it.
359
360
 
361
+ A pattern reads the way a `.gitignore` line does. One with a `/` names its
362
+ file below the root as well, so `config/database.yml` covers an engine's, a
363
+ pack's and a test/dummy app's copy, and a leading `/` anchors it at the root. A
364
+ trailing `/` names a folder and everything in it, and `**/` stands for any
365
+ number of folders, none included, so `**/id_rsa` covers an `id_rsa` at the
366
+ root too. A file a link brings in from the app's repository is also asked by
367
+ its name there, so a link to a pack's `config/` folder cannot hide the
368
+ `config/` a pattern looks for. A path a caller names that is longer than 4096
369
+ bytes, which no file system holds, is answered as not found.
370
+
360
371
  ### AI context file exclusions
361
372
 
362
373
  Search also excludes generated AI context files to prevent circular references:
@@ -376,6 +387,28 @@ AGENTS.md, opencode.json
376
387
  config.sensitive_patterns = %w[.env* *.key *.pem credentials.yml.enc]
377
388
  ```
378
389
 
390
+ On the static tier (`--no-boot`, or a boot that failed) no initializer runs.
391
+ The gem reads every Ruby file of the app that names it (`RailsAiContext` or
392
+ `rails_ai_context`), however the boot loads it, without running it: a file
393
+ under a `config/` folder, and the top level and class bodies of any other file,
394
+ which run as the file loads, whole; the body of a method outside `config/`, a
395
+ tool's or a helper's that runs when called, for what it writes. Tests (a `spec/` or `test/` folder at any
396
+ depth, an engine's test app among them), `db/`, `tmp/`, `vendor/bundle/`,
397
+ `vendor/ruby/`, `vendor/cache/`, `vendor/gems/`, an installed copy of the gem, a
398
+ generator's templates, hidden files and links are left out, and
399
+ `config/initializers` and `config/environments` are read through a link as
400
+ Rails globs them. The literal values those files write to
401
+ `sensitive_patterns`, `excluded_paths` and `skip_tools` are added to the YAML's;
402
+ a removal is left out. A value it cannot read, such as one built from a
403
+ constant or the environment, the configuration handed to other code or
404
+ reached through `send`, a pattern changed in place, or a file that does not
405
+ parse, stops the static tier with an error that names the file and line;
406
+ outside a method, so does a list or one of its patterns kept or handed on. A
407
+ file outside the app, a list a method keeps and changes later, and code that reaches
408
+ the settings through a name it builds at run time
409
+ (`const_get("Rails" + "AiContext")`) are not followed.
410
+ `.rails-ai-context.yml` is read on every tier.
411
+
379
412
  ---
380
413
 
381
414
  ## Path traversal protection
@@ -516,7 +549,7 @@ Complex patterns that would cause catastrophic backtracking raise `RegexpError`
516
549
 
517
550
  `SafeFile.read` provides drop-in safety for all file reads:
518
551
 
519
- - Size limit enforcement (`config.max_file_size`, default: 5 MB)
552
+ - Size limit enforcement (`config.max_file_size`, default: 5 MB). `rails_search_code` skips a file over it on both backends, so a row is never shown without its whole file's redaction, and a file named as its path answers `File too large`
520
553
  - Returns `nil` on any failure (no exceptions leak)
521
554
  - UTF-8 encoding with invalid/undefined byte replacement
522
555
  - Handles: ENOENT, EACCES, EISDIR, ENAMETOOLONG, SystemCallError
@@ -532,6 +565,8 @@ What gets redacted:
532
565
  - Passwords, tokens, secrets and API keys, wherever they are named
533
566
  - The same in the app's own source a tool or resource shows: a method body, a template, a test file, a scan's code line, a change's diff (where a file on `sensitive_patterns` shows none of its lines)
534
567
  - Credentials embedded in URIs (`redis://user:pass@host`)
568
+ - Values a logged SQL statement holds for a secret column: compared with it (through `LOWER()`, `CAST()` or a JSON path too), listed for it with `IN` or `BETWEEN`, inserted under it, or bound to a `$1` or `?` placeholder for it. MySQL and MariaDB log every value inline, and `insert_all` and `upsert_all` do on every adapter. A value whose column cannot be told is filtered when the statement names a secret column, and every value of an `INSERT` with no column list is filtered. A secret-named key in JSON a value holds is filtered, its quotes escaped or not. A line with `\'` in it that holds no backquoted name outside its strings is read both as MySQL and as standard SQL. Each string of a JSON log line, a header before it or not, is read as a line of its own; a value under a secret's key is filtered, an array's included, and so are a JSON logger's binds when the statement beside them names a secret column. A line is read on its own, so a value with a line break in it is read a line at a time
569
+ - The values a database error repeats: PostgreSQL's failing row whole, since it names no column, and its duplicate key when the key names a secret; MySQL's and MariaDB's duplicate entry, unless Rails named the index after columns that name no secret (`index_users_on_email`); and the excerpt of the statement a MySQL syntax error quotes, whole. A message cut off is filtered to the end of its line. An error in a language other than English is read as any other line
535
570
  - Email addresses in log lines, in any script (`jürgen@example.com`, `anna@münchen.de`)
536
571
  - Values assigned to secret-named settings in initializers
537
572
 
@@ -596,7 +631,7 @@ The engine builds one transport per process, under a mutex, outside the controll
596
631
  ## Credential handling
597
632
 
598
633
  - `rails_get_config(section:"env")` returns credential **keys** and no values
599
- - The process's environment variable values are never read. A default written in code, and a placeholder from `.env.example`, `.env.sample` or `.env.template`, is shown after redaction
634
+ - The process's environment variable values are never read. A default written in code, and a placeholder from `.env.example`, `.env.sample` or `.env.template`, is shown after redaction. A default in code, an example file's value and a Dockerfile `ENV` or `ARG` value read `[FILTERED]` when their shape can carry a secret: a URL whose user part holds a password or is a key (16 or more letters and digits, or hex in groups, as a DSN's is), a webhook's address, a token in a query, a run of 16 or more letters and digits with both in it, and a hex key split into groups. `/`, `.`, `-` and `_` end a run, so a path, a host and a slug show; so does a key written with them, base64 with a `/` among them, or in letters alone, unless its name says it is one. A class name with a digit in a run of 16 reads `[FILTERED]`. A `BUNDLE_<HOST>` variable, which Bundler reads a gem server's password from, is hidden whatever it holds. An example file's inline comment is hidden when it holds a credential, one of these shapes or a hex key in groups among its words, and a `#` starts a comment only after a space, so a password's `#` stays in its value (dotenv itself cuts an unquoted value at any `#`). A Kamal clear value that holds any URL, or a run of 16 letters and digits with `/` in it, is hidden
600
635
  - `config/credentials/*.yml.enc` is in the sensitive patterns list
601
636
 
602
637
  ---
data/docs/SETUP.md CHANGED
@@ -91,6 +91,197 @@ This is what the generator writes for an in-Gemfile install. A standalone instal
91
91
 
92
92
  On Windows the command is `cmd` and `"/c"` goes ahead of the args (`["/c", "bundle", "exec", "rails-ai-context", "serve"]`): a gem's executables there are batch files, which a client that starts its server without a shell does not run. The generator writes that form on Windows, and `doctor` names an entry written for the other platform; see [GUIDE.md](GUIDE.md#mcp-server-setup) for teams on both.
93
93
 
94
+ ### As a plugin
95
+
96
+ The repository is also a Claude Code plugin marketplace. The plugin brings the
97
+ MCP server and, at the start of each session in a Rails app, the six rules the
98
+ context files open with. It writes no context files or MCP config, so it suits
99
+ a team that keeps its own `CLAUDE.md`; the install records it in
100
+ `.claude/settings.json`.
101
+
102
+ ```bash
103
+ gem install rails-ai-context # the plugin runs the installed gem
104
+ claude plugin install rails-ai-context --marketplace crisnahine/rails-ai-context --scope project
105
+ ```
106
+
107
+ In a session the second line is `/plugin install rails-ai-context --marketplace crisnahine/rails-ai-context`.
108
+ A Claude Code too old for `--marketplace` takes two steps:
109
+ `/plugin marketplace add crisnahine/rails-ai-context`, then
110
+ `/plugin install rails-ai-context@rails-ai-context`. Project scope records the
111
+ plugin in `.claude/settings.json` of the folder you run it in, so run it in the
112
+ app's folder; commit that file, and add the marketplace there too so a
113
+ teammate's Claude Code offers to install it:
114
+
115
+ ```json
116
+ {
117
+ "enabledPlugins": { "rails-ai-context@rails-ai-context": true },
118
+ "extraKnownMarketplaces": {
119
+ "rails-ai-context": { "source": { "source": "github", "repo": "crisnahine/rails-ai-context" } }
120
+ }
121
+ }
122
+ ```
123
+
124
+ A teammate who has the committed setting runs `claude plugin install
125
+ rails-ai-context --marketplace crisnahine/rails-ai-context --scope local`,
126
+ which leaves the committed file as it is.
127
+
128
+ - **Where it runs.** Claude Code reads `.claude/settings.json` only from the
129
+ folder a session starts in, so project scope reaches sessions started in the
130
+ app's folder. For sessions started in a folder inside the app, or in a
131
+ monorepo's app folder from the repository's root, enable it with `--scope
132
+ local` (the git repository's `settings.local.json`) or at user scope. The
133
+ server then reads the app Claude Code was started in, the app above that
134
+ folder, or the one app below it, as every command does. At user scope the
135
+ plugin starts in every project; outside a Rails app its server connects with
136
+ no tools and tells the session why. In a folder that holds several apps it
137
+ says to start Claude Code in one of them; for a session over all of them,
138
+ use the [workspace](CLI.md#init-standalone-only) entries `init` writes, and
139
+ turn the plugin off for that folder with `claude plugin disable
140
+ rails-ai-context@rails-ai-context --scope project` run there, or `--scope
141
+ local` where a `--scope local` install turns it on. On macOS and Linux a
142
+ local setting written at a git repository's root reaches every session in
143
+ that repository, the apps' too. On Windows, and where the repository's root
144
+ is your home folder or belongs to another user, a local setting reaches only
145
+ sessions started in the folder it was written in. Where `init` at a
146
+ repository's root wrote an app's own `.mcp.json`, which sessions started in
147
+ the app read, a `--scope local` install that reaches those sessions runs the
148
+ plugin beside it; `doctor` names the one to turn off.
149
+ - **The gem comes first.** The plugin needs rails-ai-context 5.35.1 or later,
150
+ installed for the `ruby` on Claude Code's `PATH` (Ruby 3.1 or later); `gem
151
+ update rails-ai-context` brings an older one up. A shim version manager
152
+ (rbenv, asdf, or mise with its shims on `PATH`) picks that Ruby by the folder
153
+ Claude Code starts in: that folder's `.ruby-version` or `.tool-versions`, or
154
+ the global one, so a session started above the app runs that folder's Ruby. A
155
+ shell-hook one (rvm, chruby, `mise activate`) leaves the Ruby of the shell
156
+ that started Claude Code, and a Claude Code started from the Dock or an IDE
157
+ may see the system Ruby instead. An app that also has the gem in its Gemfile
158
+ gets its bundled copy, as with any standalone install, a bundle kept under
159
+ its own path such as `vendor/bundle` too. A copy from git is the exception:
160
+ the installed copy serves and the app's boot loads the git copy over it, so
161
+ point the Gemfile at a release or a `path:` checkout. When the plugin cannot
162
+ serve, its server still connects, with no tools, and tells the session why,
163
+ naming the Ruby and the folder: no gem for that Ruby, an older one, a Ruby
164
+ too old, a bundle Claude Code inherited from `bundle exec` that lacks the
165
+ gem, several apps or none. The same line is the server's stderr, which
166
+ `claude --debug` shows. A start that fails after that, from the gem's own
167
+ error or an inherited bundle not installed for that Ruby (Bundler's trace,
168
+ [TROUBLESHOOTING.md](TROUBLESHOOTING.md#mcp-server-fails-to-start)), is
169
+ listed as failed in `/mcp`, and Claude Code then skips the server in new
170
+ sessions for about 15 minutes: reconnect it in `/mcp` once the cause is
171
+ fixed. A session gets no rules while the server has no tools for them to
172
+ name.
173
+ - **Install the gem, not its setup, for Claude Code.** Where the app's
174
+ `.claude/settings.json` turns the plugin on, the install generator and
175
+ `init` write no `.mcp.json` server and say why, and so does `init` in a
176
+ folder holding one app, where that folder's own `.claude/settings.json`
177
+ turns it on; it still writes the app's own `.mcp.json` for sessions started
178
+ in the app, which do not read that folder's settings, and says so. Where
179
+ only your own settings turn it on, they still write one for the team, and
180
+ their next step says to turn it off for yourself with `/mcp`.
181
+ - **The plugin or another server, not both.** Claude Code starts the plugin's
182
+ server and one from `.mcp.json` or `claude mcp add`, an HTTP entry under the
183
+ gem's name included, as two servers and lists every tool twice. `doctor`
184
+ reads Claude Code the way a session does, by the folder it started in: that
185
+ folder's settings and the local settings that reach it (its git
186
+ repository's, a linked worktree's being its repository's, as "Where it runs"
187
+ says), every `.mcp.json` from that folder up, the `/mcp` toggles and local
188
+ servers Claude Code keeps for the repository, the MCP policy in
189
+ `allowedMcpServers` and `deniedMcpServers` (by name, command or url), and a
190
+ deployed `managed-mcp.json`, whose servers are then the only ones Claude
191
+ Code starts, each still subject to `/mcp` and `deniedMcpServers`, and to
192
+ `allowedMcpServers` where its entry expands a `${VAR}`, and the marketplace
193
+ policy in `strictKnownMarketplaces` and `blockedMarketplaces`. It follows a
194
+ `CLAUDE_CONFIG_DIR` set in your settings' `env` block, as Claude Code does.
195
+ It does not see settings an organization delivers from the Claude console,
196
+ through MDM or the Windows registry, and it reads the managed settings files
197
+ as if all of them were in force; where policy comes that way, its MCP
198
+ verdicts can be wrong. It checks the shape of `enabledPlugins` only, while
199
+ Claude Code ignores a whole settings file that breaks its schema elsewhere
200
+ and says so when it starts: fix such a file first. It looks at the app, the
201
+ folder `doctor` runs in, the folders above the app that hold it alone, and
202
+ the folders Claude Code keeps projects for. The plugin counts where it is
203
+ enabled, installed for that project, not turned off in `/mcp` and not
204
+ blocked by policy; then Claude Code's MCP check passes without a
205
+ `.mcp.json`, and with no AI tools chosen it checks no other tool's config
206
+ unless that config is broken. Where the plugin runs only for sessions
207
+ started in another folder, `doctor` names that folder and what turns it on
208
+ for this one. Where Claude Code never ran, as on CI, what the plugin needs
209
+ is a warning, not a failure. Where it runs nowhere, `doctor` names why: not
210
+ installed (with the install that leaves a committed file alone), a local
211
+ marketplace folder that is gone, its server turned off in `/mcp`, the MCP or
212
+ marketplace policy, a managed settings file Claude Code cannot read, or a
213
+ `CLAUDE_CONFIG_DIR` that is empty or not an absolute path, set in the
214
+ environment or a settings `env` block (`doctor` names which), which leaves
215
+ each session reading its own folder's. A second server counts where such a
216
+ session would start it, and `doctor` names the command that removes it and
217
+ the folder to run it in: a `.mcp.json` entry comes out where the committed
218
+ settings turn the plugin on, and is turned off with `/mcp` where only your
219
+ own settings do.
220
+ - **The rules load once.** The hook skips them where Claude Code already loads
221
+ the generator's rules for that session: from the app's `CLAUDE.md` or
222
+ `.claude/rules/rails-mcp-tools.md`, or from its `AGENTS.md` where
223
+ `CLAUDE.md` imports it or no `CLAUDE.md` is on the way up from the folder
224
+ the session started in, or from a `CLAUDE.md`, `.claude/CLAUDE.md` or
225
+ `CLAUDE.local.md` on that way up that carries them, as an engine's above its
226
+ test/dummy app does (your own `~/.claude/CLAUDE.md` does not count). It
227
+ skips them too where `.rails-ai-context.yml` or a configure block in
228
+ `config/application.rb`, the environment's file or an initializer sets
229
+ `anti_hallucination_rules` to `false` or `nil`; a block's value wins over
230
+ the file's, as when the app boots. It reads that Ruby without running it, so
231
+ only a literal that ends its statement counts, and rule 3 leaves out the
232
+ `rails_get_config` sections `skip_tools` turns off, in the YAML or a literal
233
+ list, assigned or appended (`=`, `<<` chained or not, `+=`, `|=`, `push`,
234
+ `append`, `concat`); a symbol appended in place, a Set or a value built at
235
+ boot is not read, as the server skips none of those either. A session
236
+ started above the app gets the rules from the hook, and once a file in the
237
+ app is read, Claude Code also loads that app's `CLAUDE.md`, so such a
238
+ session can hold them twice. So can a session resumed after the app's
239
+ `CLAUDE.md` gained the rules (from `init`, `context` or a pull), until
240
+ `/clear` or a compaction.
241
+ - **Windows works the same.** The server starts `ruby` without a shell, so it
242
+ needs no `cmd /c`.
243
+ - **One allowlist entry lasts.** The server's command line names no plugin
244
+ folder, so no update changes it, and under an `allowedMcpServers` list this
245
+ entry admits it (`doctor` names it where it is missing). It admits any plugin
246
+ that ships the same line, too. Where the list admits other stdio servers by
247
+ `serverName`, adding this entry switches every stdio server to matching by
248
+ command, so each of them needs a `serverCommand` entry as well; `doctor`
249
+ says so. Naming no script also keeps rbenv from picking the Ruby by the
250
+ plugin's own folder.
251
+
252
+ ```json
253
+ {"serverCommand":["ruby","-e","load File.join(ENV.fetch('CLAUDE_PLUGIN_ROOT'), 'scripts', 'serve.rb')"]}
254
+ ```
255
+ - **Permission rules name the plugin's server.** Its tools are
256
+ `mcp__plugin_rails-ai-context_rails-ai-context__<tool>`, so a rule for the
257
+ `.mcp.json` server (`mcp__rails-ai-context`) does not cover them;
258
+ `mcp__plugin_rails-ai-context_rails-ai-context` allows them all.
259
+ - **A slow app's boot.** The plugin's server entry is the plugin's own, so set
260
+ `RAILS_AI_CONTEXT_BOOT_TIMEOUT` and `MCP_TIMEOUT` in Claude Code's settings
261
+ `env` (user, project or local) or in the shell that starts Claude Code; see
262
+ [TROUBLESHOOTING.md](TROUBLESHOOTING.md#answers-say-boot-did-not-finish-within-20s).
263
+ - **Updates follow this repository.** The plugin's version is the commit it was
264
+ installed from. Claude Code updates it only with auto-update on (under
265
+ `/plugin`, Marketplaces) or `claude plugin update
266
+ rails-ai-context@rails-ai-context`. The server is versioned by the gem: `gem
267
+ update rails-ai-context`, then reconnect the server in `/mcp` or start a new
268
+ session. Run `gem cleanup` only after that: a running server loads its files
269
+ as it needs them, so once its copy is gone, a call that needs a file it has
270
+ not loaded yet answers with a request to reconnect it.
271
+ - **Removing it** from one app is `claude plugin disable
272
+ rails-ai-context@rails-ai-context --scope local` run there, which wins over
273
+ your own and the committed settings for every session the local setting
274
+ reaches ("Where it runs"). Taking an install out is `claude plugin uninstall
275
+ rails-ai-context@rails-ai-context --scope <scope>`, with the scope it went in
276
+ at, run in the folder it was installed from: once for a user install, once
277
+ per project for a project or local one. A marketplace added from a local
278
+ folder or file runs the plugin with no install, so there only the disable
279
+ turns it off. The gem's `uninstall` leaves Claude Code's
280
+ settings alone and names those commands, one disable per repository whose
281
+ sessions run the plugin, before it suggests removing the gem, which the
282
+ plugin runs; where managed settings turn the plugin on, or a deployed
283
+ `managed-mcp.json` runs the gem, it says to ask whoever manages them.
284
+
94
285
  ### Split rules with `paths:` frontmatter
95
286
 
96
287
  Claude Code loads `.claude/rules/` files conditionally based on YAML frontmatter: