Filter sensitive information from RubyLLM conversations using Top Secret.
Add this line to your application's Gemfile:
gem "ruby_llm-top_secret"Then run:
bundle installRequiring the gem patches RubyLLM::Chat to support filtering sensitive information before it reaches the LLM provider. Filtering is opt-in per conversation using with_filtering.
RubyLLM::TopSecret.with_filtering do
chat = RubyLLM.chat
response = chat.ask("My name is Ralph and my email is ralph@thoughtbot.com")
# The provider receives: "My name is [PERSON_1] and my email is [EMAIL_1]"
# The response comes back with placeholders restored:
puts response.content
# => "Nice to meet you, Ralph!"
endWithout with_filtering, conversations behave normally with no filtering overhead.
- Wrap your conversation in
RubyLLM::TopSecret.with_filtering - Before sending to the provider, all messages are filtered using
TopSecret::Text.filter_all - The provider only sees placeholders like
[PERSON_1]and[EMAIL_1] - The response is restored using
TopSecret::FilteredText.restore - Original message content is always preserved locally
Filtering state is thread-isolated, so concurrent requests in a web server won't interfere with each other.
In Rails apps with acts_as_chat, declare acts_as_filtered_chat on your model so that filtering works automatically — including from background jobs where a with_filtering block isn't possible.
class Chat < ApplicationRecord
acts_as_chat
acts_as_filtered_chat
endEvery call to complete on this model will filter automatically. The restored (not filtered) response is what gets saved to your database.
Note
When filtering is active, the assistant message is written to the database twice — once by RubyLLM's built-in callback (with filtered placeholders), and again by this gem (with restored content). This is a known limitation of the current architecture.
To control filtering per chat, pass an if: condition with a Symbol or Proc. The gem does not provide a database column — your application is responsible for storing the decision and exposing it via a method on the model.
-
Add a boolean column to your chats table
rails generate migration AddFilteredToChats filtered:boolean -
Pass
if: :filtered?toacts_as_chatclass Chat < ApplicationRecord acts_as_chat acts_as_filtered_chat if: :filtered? end
Note
The if: option follows the same convention as Rails callbacks — it accepts a Symbol (method name) or a Proc:
acts_as_filtered_chat if: -> { filtered? }Errors from Top Secret (filtering or restoring failures) are wrapped in RubyLLM::TopSecret::Error. Errors from RubyLLM itself (API failures, etc.) are passed through unchanged.
RubyLLM::TopSecret.with_filtering do
chat.ask("Hello")
rescue RubyLLM::TopSecret::Error => e
# Top Secret failed (e.g., NER model missing)
rescue RubyLLM::Error => e
# RubyLLM API error
endAfter checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org
Bug reports and pull requests are welcome on GitHub at https://github.com/thoughtbot/ruby_llm-top_secret.
Please create a new discussion if you want to share ideas for new features.
ruby_llm-top_secret is Copyright (c) thoughtbot, inc. It is free software, and may be redistributed under the terms specified in the LICENSE file.
This repo is maintained and funded by thoughtbot, inc. The names and logos for thoughtbot are trademarks of thoughtbot, inc.
We love open source software! See our other projects. We are available for hire.