{"id":30415467,"url":"https://github.com/thoughtbot/top_secret","last_synced_at":"2025-10-30T20:20:11.028Z","repository":{"id":308921981,"uuid":"1024883536","full_name":"thoughtbot/top_secret","owner":"thoughtbot","description":"Filter sensitive information from free text before sending it to external services or APIs, such as chatbots and LLMs.","archived":false,"fork":false,"pushed_at":"2025-08-15T18:33:41.000Z","size":79,"stargazers_count":39,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-08-15T18:37:41.618Z","etag":null,"topics":["ner","pii-detection","redaction","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/thoughtbot.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":"CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":"thoughtbot"}},"created_at":"2025-07-23T11:47:05.000Z","updated_at":"2025-08-15T17:00:01.000Z","dependencies_parsed_at":"2025-08-08T18:29:18.914Z","dependency_job_id":"52c4231c-919b-43bd-975a-30f262107d35","html_url":"https://github.com/thoughtbot/top_secret","commit_stats":null,"previous_names":["thoughtbot/top_secret"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/thoughtbot/top_secret","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoughtbot%2Ftop_secret","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoughtbot%2Ftop_secret/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoughtbot%2Ftop_secret/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoughtbot%2Ftop_secret/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thoughtbot","download_url":"https://codeload.github.com/thoughtbot/top_secret/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thoughtbot%2Ftop_secret/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":270655377,"owners_count":24623166,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-15T02:00:12.559Z","response_time":110,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ner","pii-detection","redaction","ruby"],"created_at":"2025-08-22T04:02:21.077Z","updated_at":"2025-10-30T20:20:11.014Z","avatar_url":"https://github.com/thoughtbot.png","language":"Ruby","readme":"# Top Secret\n\n[![Ruby](https://github.com/thoughtbot/top_secret/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/thoughtbot/top_secret/actions/workflows/main.yml)\n\nFilter sensitive information from free text before sending it to external services or APIs, such as chatbots and LLMs.\n\nBy default it filters the following:\n\n-   Credit cards\n-   Emails\n-   Phone numbers\n-   Social security numbers\n-   People's names\n-   Locations\n\nHowever, you can add your own [custom filters](#custom-filters).\n\n## Installation\n\nInstall the gem and add to the application's Gemfile by executing:\n\n```bash\nbundle add top_secret\n```\n\nIf bundler is not being used to manage dependencies, install the gem by executing:\n\n```bash\ngem install top_secret\n```\n\n\u003e [!IMPORTANT]\n\u003e Top Secret depends on [MITIE Ruby][], which depends on [MITIE][].\n\u003e\n\u003e You'll need to download and extract [ner_model.dat][] first.\n\n\u003e [!TIP]\n\u003e Due to its large size, you'll likely want to avoid committing [ner_model.dat][] into version control.\n\u003e\n\u003e You'll need to ensure the file exists in deployed environments. See relevant [discussion][discussions_60] for details.\n\u003e\n\u003e Alternatively, you can disable NER filtering entirely by setting `model_path` to `nil` if you only need regex-based filters (credit cards, emails, phone numbers, SSNs). This improves performance and eliminates the model file dependency.\n\nBy default, Top Secret assumes the file will live at the root of your project, but this can be configured.\n\n```ruby\nTopSecret.configure do |config|\n  config.model_path = \"path/to/ner_model.dat\"\nend\n```\n\n## Default Filters\n\nTop Secret ships with a set of filters to detect and redact the most common types of sensitive information.\n\nYou can [override](#overriding-the-default-filters-1), [disable](#disabling-a-default-filter-1), or [add](#adding-new-default-filters) to this list as needed.\n\nBy default, the following filters are enabled\n\n**`credit_card_filter`**\n\nMatches common credit card formats\n\n```ruby\nresult = TopSecret::Text.filter(\"My card number is 4242-4242-4242-4242\")\nresult.output\n\n# =\u003e \"My card number is [CREDIT_CARD_1]\"\n```\n\n**`email_filter`**\n\nMatches email addresses\n\n```ruby\nresult = TopSecret::Text.filter(\"Email me at ralph@thoughtbot.com\")\nresult.output\n\n# =\u003e \"Email me at [EMAIL_1]\"\n```\n\n**`phone_number_filter`**\n\nMatches phone numbers\n\n```ruby\nresult = TopSecret::Text.filter(\"Call me at 555-555-5555\")\nresult.output\n\n# =\u003e \"Call me at [PHONE_NUMBER_1]\"\n```\n\n**`ssn_filter`**\n\nMatches U.S. Social Security numbers\n\n```ruby\nresult = TopSecret::Text.filter(\"My SSN is 123-45-6789\")\nresult.output\n\n# =\u003e \"My SSN is [SSN_1]\"\n```\n\n**`people_filter`**\n\nDetects names of people (NER-based)\n\n```ruby\nresult = TopSecret::Text.filter(\"Ralph is joining the meeting\")\nresult.output\n\n# =\u003e \"[PERSON_1] is joining the meeting\"\n```\n\n**`location_filter`**\n\nDetects location names (NER-based)\n\n```ruby\nresult = TopSecret::Text.filter(\"Let's meet in Boston\")\nresult.output\n\n# =\u003e \"Let's meet in [LOCATION_1]\"\n```\n\n## Usage\n\n```ruby\nTopSecret::Text.filter(\"Ralph can be reached at ralph@thoughtbot.com\")\n```\n\nThis will return\n\n```ruby\n\u003cTopSecret::Text::Result\n  @input=\"Ralph can be reached at ralph@thoughtbot.com\",\n  @mapping={:EMAIL_1=\u003e\"ralph@thoughtbot.com\", :PERSON_1=\u003e\"Ralph\"},\n  @output=\"[PERSON_1] can be reached at [EMAIL_1]\"\n\u003e\n```\n\nView the original text\n\n```ruby\nresult.input\n\n# =\u003e \"Ralph can be reached at ralph@thoughtbot.com\"\n```\n\nView the filtered text\n\n```ruby\nresult.output\n\n# =\u003e \"[PERSON_1] can be reached at [EMAIL_1]\"\n```\n\nView the mapping\n\n```ruby\nresult.mapping\n\n# =\u003e {:EMAIL_1=\u003e\"ralph@thoughtbot.com\", :PERSON_1=\u003e\"Ralph\"}\n```\n\nCheck if sensitive information was found\n\n```ruby\nresult.sensitive?\n\n# =\u003e true\n\nresult.safe?\n\n# =\u003e false\n```\n\n### Scanning for Sensitive Information\n\nUse `TopSecret::Text.scan` to detect sensitive information without redacting the text. This is useful when you only need to check if sensitive data exists or get a mapping of what was found:\n\n```ruby\nTopSecret::Text.scan(\"Ralph can be reached at ralph@thoughtbot.com\")\n```\n\nThis will return\n\n```ruby\n\u003cTopSecret::Text::ScanResult\n  @mapping={:EMAIL_1=\u003e\"ralph@thoughtbot.com\", :PERSON_1=\u003e\"Ralph\"}\n\u003e\n```\n\nCheck if sensitive information was found\n\n```ruby\nresult.sensitive?\n\n# =\u003e true\n\nresult.safe?\n\n# =\u003e false\n```\n\nView the mapping of found sensitive information\n\n```ruby\nresult.mapping\n\n# =\u003e {:EMAIL_1=\u003e\"ralph@thoughtbot.com\", :PERSON_1=\u003e\"Ralph\"}\n```\n\nThe `scan` method accepts the same filter options as `filter`:\n\n```ruby\n# Override default filters\nemail_filter =  TopSecret::Filters::Regex.new(\n  label: \"EMAIL_ADDRESS\",\n  regex: /\\w+\\[at\\]\\w+\\.\\w+/\n)\nresult = TopSecret::Text.scan(\"Contact user[at]example.com\", email_filter:)\nresult.mapping\n# =\u003e {:EMAIL_ADDRESS_1=\u003e\"user[at]example.com\"}\n\n# Disable specific filters\nresult = TopSecret::Text.scan(\"Ralph works in Boston\", people_filter: nil)\nresult.mapping\n# =\u003e {:LOCATION_1=\u003e\"Boston\"}\n\n# Add custom filters\nip_filter = TopSecret::Filters::Regex.new(\n  label: \"IP_ADDRESS\",\n  regex: /\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b/\n)\nresult = TopSecret::Text.scan(\"Server IP is 192.168.1.1\", custom_filters: [ip_filter])\nresult.mapping\n# =\u003e {:IP_ADDRESS_1=\u003e\"192.168.1.1\"}\n```\n\n### Batch Processing\n\nWhen processing multiple messages, use `filter_all` to ensure consistent redaction labels across all messages:\n\n```ruby\nmessages = [\n  \"Contact ralph@thoughtbot.com for details\",\n  \"Email ralph@thoughtbot.com again if needed\",\n  \"Also CC ruby@thoughtbot.com on the thread\"\n]\n\nresult = TopSecret::Text.filter_all(messages)\n```\n\nThis will return\n\n```ruby\n\u003cTopSecret::Text::BatchResult\n  @mapping={:EMAIL_1=\u003e\"ralph@thoughtbot.com\", :EMAIL_2=\u003e\"ruby@thoughtbot.com\"},\n  @items=[\n    \u003cTopSecret::Text::Result @input=\"Contact ralph@thoughtbot.com for details\", @output=\"Contact [EMAIL_1] for details\", @mapping={:EMAIL_1=\u003e\"ralph@thoughtbot.com\"}\u003e,\n    \u003cTopSecret::Text::Result @input=\"Email ralph@thoughtbot.com again if needed\", @output=\"Email [EMAIL_1] again if needed\", @mapping={:EMAIL_1=\u003e\"ralph@thoughtbot.com\"}\u003e,\n    \u003cTopSecret::Text::Result @input=\"Also CC ruby@thoughtbot.com on the thread\", @output=\"Also CC [EMAIL_2] on the thread\", @mapping={:EMAIL_2=\u003e\"ruby@thoughtbot.com\"}\u003e\n  ]\n\u003e\n```\n\nAccess the global mapping\n\n```ruby\nresult.mapping\n\n# =\u003e {:EMAIL_1=\u003e\"ralph@thoughtbot.com\", :EMAIL_2=\u003e\"ruby@thoughtbot.com\"}\n```\n\nAccess individual items\n\n```ruby\nresult.items[0].input\n# =\u003e \"Contact ralph@thoughtbot.com for details\"\n\nresult.items[0].output\n# =\u003e \"Contact [EMAIL_1] for details\"\n\nresult.items[0].mapping\n# =\u003e {:EMAIL_1=\u003e\"ralph@thoughtbot.com\"}\n\nresult.items[0].sensitive?\n# =\u003e true\n\nresult.items[0].safe?\n# =\u003e false\n```\n\nThe key benefit is that identical values receive the same labels across all messages - notice how `ralph@thoughtbot.com` becomes `[EMAIL_1]` in both the first and second messages.\n\nEach item also maintains its own mapping containing only the sensitive information found in that specific message, while the batch result provides a global mapping of all sensitive information across all messages.\n\n### Restoring Filtered Text\n\nWhen external services (like LLMs) return responses containing filter placeholders, use `TopSecret::FilteredText.restore` to substitute them back with original values:\n\n```ruby\n# Filter messages before sending to LLM\nmessages = [\"Contact ralph@thoughtbot.com for details\"]\nbatch_result = TopSecret::Text.filter_all(messages)\n\n# Send filtered text to LLM: \"Contact [EMAIL_1] for details\"\n# LLM responds with: \"I'll email [EMAIL_1] about this request\"\nllm_response = \"I'll email [EMAIL_1] about this request\"\n\n# Restore the original values\nrestore_result = TopSecret::FilteredText.restore(llm_response, mapping: batch_result.mapping)\n```\n\nThis will return\n\n```ruby\n\u003cTopSecret::FilteredText::Result\n  @output=\"I'll email ralph@thoughtbot.com about this request\",\n  @restored=[\"[EMAIL_1]\"],\n  @unrestored=[]\n\u003e\n```\n\nAccess the restored text\n\n```ruby\nrestore_result.output\n# =\u003e \"I'll email ralph@thoughtbot.com about this request\"\n```\n\nTrack which placeholders were restored\n\n```ruby\nrestore_result.restored\n# =\u003e [\"[EMAIL_1]\"]\n\nrestore_result.unrestored\n# =\u003e []\n```\n\nThe restoration process tracks both successful and failed placeholder substitutions, allowing you to handle cases where the LLM response contains placeholders not found in your mapping.\n\n### Working with LLMs\n\nWhen sending filtered information to LLMs, they'll likely need to be instructed on how to handle those filters. Otherwise, we risk them not being returned in the response, which would break the restoration process.\n\nHere's a recommended approach:\n\n```ruby\ninstructions = \u003c\u003c~TEXT\n  I'm going to send filtered information to you in the form of free text.\n  If you need to refer to the filtered information in a response, just reference it by the filter.\nTEXT\n```\n\nComplete example:\n\n```ruby\nrequire \"openai\"\nrequire \"top_secret\"\n\nopenai = OpenAI::Client.new(\n  api_key: Rails.application.credentials.openai.api_key!\n)\n\noriginal_messages = [\n  \"Ralph lives in Boston.\",\n  \"You can reach them at ralph@thoughtbot.com or 877-976-2687\"\n]\n\n# Filter all messages\nresult = TopSecret::Text.filter_all(original_messages)\nfiltered_messages = result.items.map(\u0026:output)\n\nuser_messages = filtered_messages.map { {role: \"user\", content: it} }\n\n# Instruct LLM how to handle filtered messages\ninstructions = \u003c\u003c~TEXT\n  I'm going to send filtered information to you in the form of free text.\n  If you need to refer to the filtered information in a response, just reference it by the filter.\nTEXT\n\nmessages = [\n  {role: \"system\", content: instructions},\n  *user_messages\n]\n\nchat_completion = openai.chat.completions.create(messages:, model: :\"gpt-5\")\nresponse = chat_completion.choices.last.message.content\n\n# Restore the response from the mapping\nmapping = result.mapping\nrestored_response = TopSecret::FilteredText.restore(response, mapping:).output\n\nputs(restored_response)\n```\n\n### Advanced Examples\n\n#### Overriding the default filters\n\nWhen overriding or [disabling](#disabling-a-default-filter-1) a [default filter](#default-filters), you must map to the correct key.\n\n\u003e [!IMPORTANT]\n\u003e Invalid filter keys will raise an `ArgumentError`. Only the following keys are valid:\n\u003e `credit_card_filter`, `email_filter`, `phone_number_filter`, `ssn_filter`, `people_filter`, `location_filter`\n\n```ruby\nregex_filter = TopSecret::Filters::Regex.new(label: \"EMAIL_ADDRESS\", regex: /\\b\\w+\\[at\\]\\w+\\.\\w+\\b/)\nner_filter = TopSecret::Filters::NER.new(label: \"NAME\", tag: :person, min_confidence_score: 0.25)\n\nTopSecret::Text.filter(\"Ralph can be reached at ralph[at]thoughtbot.com\",\n  email_filter: regex_filter,\n  people_filter: ner_filter\n)\n```\n\nThis will return\n\n```ruby\n\u003cTopSecret::Text::Result\n  @input=\"Ralph can be reached at ralph[at]thoughtbot.com\",\n  @mapping={:EMAIL_ADDRESS_1=\u003e\"ralph[at]thoughtbot.com\", :NAME_1=\u003e\"Ralph\", :NAME_2=\u003e\"ralph[\"},\n  @output=\"[NAME_1] can be reached at [EMAIL_ADDRESS_1]\"\n\u003e\n```\n\n#### Disabling a default filter\n\n```ruby\nTopSecret::Text.filter(\"Ralph can be reached at ralph@thoughtbot.com\",\n  email_filter: nil,\n  people_filter: nil\n)\n```\n\nThis will return\n\n```ruby\n\u003cTopSecret::Text::Result\n  @input=\"Ralph can be reached at ralph@thoughtbot.com\",\n  @mapping={},\n  @output=\"Ralph can be reached at ralph@thoughtbot.com\"\n\u003e\n```\n\n#### Error handling for invalid filter keys\n\n```ruby\n# This will raise ArgumentError: Unknown key: :invalid_filter. Valid keys are: ...\nTopSecret::Text.filter(\"some text\", invalid_filter: some_filter)\n```\n\n### Custom Filters\n\n#### Adding new [Regex filters][]\n\n```ruby\nip_address_filter = TopSecret::Filters::Regex.new(\n  label: \"IP_ADDRESS\",\n  regex: /\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b/\n)\n\nTopSecret::Text.filter(\"Ralph's IP address is 192.168.1.1\",\n  custom_filters: [ip_address_filter]\n)\n```\n\nThis will return\n\n```ruby\n\u003cTopSecret::Text::Result\n  @input=\"Ralph's IP address is 192.168.1.1\",\n  @mapping={:PERSON_1=\u003e\"Ralph\", :IP_ADDRESS_1=\u003e\"192.168.1.1\"},\n  @output=\"[PERSON_1]'s IP address is [IP_ADDRESS_1]\"\n\u003e\n```\n\n#### Adding new [NER filters][]\n\nSince [MITIE Ruby][] has an API for [training][train] a model, you're free to add new NER filters.\n\n```ruby\nlanguage_filter = TopSecret::Filters::NER.new(\n  label: \"LANGUAGE\",\n  tag: :language,\n  min_confidence_score: 0.75\n)\n\nTopSecret::Text.filter(\"Ralph's favorite programming language is Ruby.\",\n  custom_filters: [language_filter]\n)\n```\n\nThis will return\n\n```ruby\n\u003cTopSecret::Text::Result\n  @input=\"Ralph's favorite programming language is Ruby.\",\n  @mapping={:PERSON_1=\u003e\"Ralph\", :LANGUAGE_1=\u003e\"Ruby\"},\n  @output=\"[PERSON_1]'s favorite programming language is [LANGUAGE_1]\"\n\u003e\n```\n\n## How Filters Work\n\nTop Secret uses two types of filters to detect and redact sensitive information:\n\n### `TopSecret::Filters::Regex`\n\n`Regex` filters use regular expressions to find patterns in text.\nThey are useful for structured data like credit card numbers, emails, or IP addresses.\n\n```ruby\nregex_filter = TopSecret::Filters::Regex.new(\n  label: \"IP_ADDRESS\",\n  regex: /\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b/\n)\n\nresult = TopSecret::Text.filter(\"Server IP: 192.168.1.1\",\n  custom_filters: [regex_filter]\n)\n\nresult.output\n# =\u003e \"Server IP: [IP_ADDRESS_1]\"\n```\n\n### `TopSecret::Filters::NER`\n\n`NER` (Named Entity Recognition) filters use the [MITIE][] library to detect entities like people, locations, and other categories based on trained language models.\nThey are ideal for free-form text where patterns are less predictable.\n\n```ruby\nner_filter = TopSecret::Filters::NER.new(\n  label: \"PERSON\",\n  tag: :person,\n  min_confidence_score: 0.25\n)\n\nresult = TopSecret::Text.filter(\"Ralph and Ruby work at thoughtbot.\",\n  people_filter: ner_filter\n)\n\nresult.output\n# =\u003e \"[PERSON_1] and [PERSON_2] work at thoughtbot.\"\n```\n\n`NER` filters match based on the tag you specify (`:person`, `:location`, etc.) and only include matches with a confidence score above `min_confidence_score`.\n\n#### Supported NER Tags\n\nBy default, Top Secret only ships with `NER` filters for two entity types:\n\n-   `:person`\n-   `:location`\n\nIf you need other tags you can [train your own MITIE model][train] and add custom NER filters:\n\n## Configuration\n\n### Overriding the model path\n\n```ruby\nTopSecret.configure do |config|\n  config.model_path = \"path/to/ner_model.dat\"\nend\n```\n\n### Disabling NER filtering\n\nFor improved performance or when the MITIE model file cannot be deployed, you can disable NER-based filtering entirely. This will disable people and location detection but retain all regex-based filters (credit cards, emails, phone numbers, SSNs):\n\n```ruby\nTopSecret.configure do |config|\n  config.model_path = nil\nend\n```\n\nThis is useful in environments where:\n\n-   The model file cannot be deployed due to size constraints\n-   You only need regex-based filtering\n-   You want to optimize for performance over NER capabilities\n\n### Overriding the confidence score\n\n```ruby\nTopSecret.configure do |config|\n  config.min_confidence_score = 0.75\nend\n```\n\n### Overriding the default filters\n\n```ruby\nTopSecret.configure do |config|\n  config.email_filter = TopSecret::Filters::Regex.new(\n    label: \"EMAIL_ADDRESS\",\n    regex: /\\b\\w+\\[at\\]\\w+\\.\\w+\\b/\n  )\nend\n```\n\n### Disabling a default filter\n\n```ruby\nTopSecret.configure do |config|\n  config.email_filter = nil\nend\n```\n\n### Adding custom filters globally\n\n```ruby\nip_address_filter = TopSecret::Filters::Regex.new(\n  label: \"IP_ADDRESS\",\n  regex: /\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b/\n)\n\nTopSecret.configure do |config|\n  config.custom_filters \u003c\u003c ip_address_filter\nend\n```\n\n## Development\n\nAfter 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.\n\n\u003e [!IMPORTANT]\n\u003e Top Secret depends on [MITIE Ruby][], which depends on [MITIE][].\n\u003e\n\u003e You'll need to download and extract [ner_model.dat][] first, and place it in the root of this project.\n\n### Performance Benchmarks\n\nRun `bin/benchmark` to test performance and catch regressions:\n\n```bash\nbin/benchmark  # CI-optimized benchmark with pass/fail thresholds\n```\n\n\u003e [!NOTE]\n\u003e When adding new public methods to the API, ensure they are included in the benchmark script to catch performance regressions.\n\nTo 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](https://rubygems.org).\n\n## Contributing\n\n[Bug reports](https://github.com/thoughtbot/top_secret/issues/new?template=bug_report.md) and [pull requests](https://github.com/thoughtbot/top_secret/pulls) are welcome on GitHub at [https://github.com/thoughtbot/top_secret](https://github.com/thoughtbot/top_secret).\n\nPlease create a [new discussion](https://github.com/thoughtbot/top_secret/discussions/new?category=ideas) if you want to share ideas for new features.\n\nThis project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/thoughtbot/top_secret/blob/main/CODE_OF_CONDUCT.md).\n\n## License\n\nOpen source templates are Copyright (c) thoughtbot, inc.\nIt contains free software that may be redistributed under the terms specified in the [LICENSE](https://github.com/thoughtbot/top_secret/blob/main/LICENSE.txt) file.\n\n## Code of Conduct\n\nEveryone interacting in the TopSecret project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/thoughtbot/top_secret/blob/main/CODE_OF_CONDUCT.md).\n\n\u003c!-- START /templates/footer.md --\u003e\n\n## About thoughtbot\n\n![thoughtbot](https://thoughtbot.com/thoughtbot-logo-for-readmes.svg)\n\nThis repo is maintained and funded by thoughtbot, inc.\nThe names and logos for thoughtbot are trademarks of thoughtbot, inc.\n\nWe love open source software!\nSee [our other projects][community].\nWe are [available for hire][hire].\n\n[community]: https://thoughtbot.com/community?utm_source=github\n[hire]: https://thoughtbot.com/hire-us?utm_source=github\n\n\u003c!-- END /templates/footer.md --\u003e\n\n[MITIE Ruby]: https://github.com/ankane/mitie-ruby\n[MITIE]: https://github.com/mit-nlp/MITIE\n[ner_model.dat]: https://github.com/mit-nlp/MITIE/releases/download/v0.4/MITIE-models-v0.2.tar.bz2\n[train]: https://github.com/ankane/mitie-ruby?tab=readme-ov-file#training\n[Regex filters]: https://github.com/thoughtbot/top_secret/blob/main/lib/top_secret/filters/regex.rb\n[NER filters]: https://github.com/thoughtbot/top_secret/blob/main/lib/top_secret/filters/ner.rb\n[discussions_60]: https://github.com/thoughtbot/top_secret/discussions/60\n","funding_links":["https://github.com/sponsors/thoughtbot"],"categories":["Ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthoughtbot%2Ftop_secret","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthoughtbot%2Ftop_secret","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthoughtbot%2Ftop_secret/lists"}