Protect sensitive values in your environment files with intelligent, blazingly fast masking.
Installation • Quick Start • Configuration • Modules • Modes • ecolog Integration • API • vs cloak.nvim
- Secure — Never leak API keys in meetings, screen shares, or pair programming sessions
- Fast — Rust-native parsing, 3-12x faster than alternatives
- Instant — Zero debounce, masks update as you type
- Smart — Only re-processes changed lines, not the entire buffer
- Compliant — Full EDF support for quotes, escapes, and multi-line values
- Extensible — Custom modes with a simple factory pattern
Requirements: Neovim 0.9+, Rust (for building)
{
"ph1losof/shelter.nvim",
lazy = false,
config = function()
require("shelter").setup({})
end,
}The native library is built automatically on first setup if Rust is installed. If the auto-build fails, then run :ShelterBuild manually.
use {
"ph1losof/shelter.nvim",
config = function()
require("shelter").setup({})
end,
}-- Minimal setup - masks all .env files in buffers
require("shelter").setup({})
-- With picker integration (Telescope, FZF, Snacks)
require("shelter").setup({
modules = {
files = true,
telescope_previewer = true,
},
})
-- With partial masking (show first/last characters)
require("shelter").setup({
default_mode = "partial",
})
-- With ecolog.nvim integration
require("shelter").setup({
modules = {
files = true,
ecolog = true, -- Mask LSP completions and hover
},
})| Command | Description |
|---|---|
:Shelter toggle [module] |
Toggle masking on/off |
:Shelter enable [module] |
Enable masking |
:Shelter disable [module] |
Disable masking |
:Shelter peek |
Reveal value while cursor is on it |
:Shelter info |
Show status and modes |
:Shelter build |
Rebuild native library |
require("shelter").setup({
-- Appearance
mask_char = "*", -- Character used for masking
highlight_group = "Comment", -- Highlight group for masked text
-- Behavior
skip_comments = true, -- Don't mask commented lines
default_mode = "full", -- "full", "partial", "none", or custom
env_filetypes = { "dotenv", "edf", "sh", "conf" }, -- Filetypes to mask
-- Module toggles (see Modules section for details)
modules = {
files = true, -- Buffer masking
telescope_previewer = false,
fzf_previewer = false,
snacks_previewer = false,
oil_previewer = false,
ecolog = false, -- ecolog.nvim integration
},
-- Pattern-based mode selection
patterns = {
["*_KEY"] = "full", -- Full mask for API keys
["*_PUBLIC*"] = "none", -- Don't mask public values
["DEBUG"] = "none", -- Don't mask debug flags
},
-- Source file-based mode selection
sources = {
[".env.local"] = "none", -- Don't mask local dev file
[".env.production"] = "full", -- Full mask for production
},
-- Mode configuration (see Modes section)
modes = {
full = { preserve_length = true },
partial = { show_start = 3, show_end = 3 },
},
})Neovim's default filetype detection doesn't assign dotenv to .env files out of
the box; variants like .env.local or .env.production can also be inconsistent
or detected correctly only after the file is opened.
This matters because we rely on Neovim's filetype detection, so if a file you'd like to shelter is not explicitly mapped to a filetype, shelter.nvim may not be active at all or may not work correctly in previews.
Custom mappings can be added using the vim.filetype.add API. For example, you
can create a file in your Neovim runtime path (usually ~/.config/nvim/filetype.lua)
and add the following:
vim.filetype.add({
-- Mappings based on file extension
extension = {
env = "dotenv",
},
-- Mappings based on FULL filename
filename = {
[".env"] = "dotenv",
["env"] = "dotenv",
},
-- Mappings based on filename pattern match
pattern = {
-- Match filenames like ".env.development", "env.local" and so on
[".?env.*"] = "dotenv",
},
})This can be done with any filetype you want, but don't forget to add them to the
env_filetypes configuration option as well!
Modules control which contexts shelter.nvim masks values in.
Buffer masking for .env files opened in Neovim.
modules = {
files = true, -- Simple enable
-- Or with options:
files = {
shelter_on_leave = true, -- Re-mask when leaving buffer (default: true)
disable_cmp = true, -- Disable completion in .env files (default: true)
},
}Features:
- Instant masking as you type
- Line-specific updates (only changed lines re-masked)
- Peek functionality to reveal current value while cursor stays on it
- Optional completion disable to prevent plugins from exposing values
Mask values in Telescope file previews.
modules = {
telescope_previewer = true,
}When enabled, .env files shown in Telescope's preview window will have their values masked.
Mask values in fzf-lua file previews.
modules = {
fzf_previewer = true,
}Mask values in Snacks.nvim file previews.
modules = {
snacks_previewer = true,
}Mask values in oil.nvim file previews.
modules = {
oil_previewer = true,
}Integration with ecolog.nvim for LSP-based environment variable management.
modules = {
ecolog = true, -- Enable all contexts
-- Or with fine-grained control:
ecolog = {
cmp = true, -- Mask completion item values (default: true)
peek = true, -- Mask hover/peek content (default: true)
picker = true, -- Mask variable picker entries (default: true)
},
}See ecolog Integration for detailed setup.
| Mode | Example | Description |
|---|---|---|
full |
secret123 → ********* |
Mask all characters |
partial |
secret123 → sec****123 |
Show start/end |
none |
secret123 → secret123 |
No masking |
modes = {
full = {
mask_char = "*",
preserve_length = true,
-- fixed_length = 8, -- Use fixed length instead
},
partial = {
show_start = 3,
show_end = 3,
min_mask = 3,
fallback_mode = "full", -- Use full mode for short values
},
}require("shelter").setup({
modes = {
redact = {
description = "Replace with [REDACTED]",
apply = function(self, ctx)
return "[REDACTED]"
end,
},
truncate = {
description = "Truncate with suffix",
schema = {
max_length = { type = "number", default = 5 },
suffix = { type = "string", default = "..." },
},
apply = function(self, ctx)
local max = self.options.max_length
if #ctx.value <= max then
return ctx.value
end
return ctx.value:sub(1, max) .. self.options.suffix
end,
},
},
patterns = {
["*_TOKEN"] = "truncate",
},
})The ctx parameter in custom modes:
---@class ShelterModeContext
---@field key string -- Variable name (e.g., "API_KEY")
---@field value string -- Original value
---@field source string|nil -- File path
---@field line_number number -- Line in file
---@field quote_type number -- 0=none, 1=single, 2=double
---@field is_comment boolean -- In a comment?
---@field config table -- Plugin configpatterns = {
["*_KEY"] = "full", -- API_KEY, SECRET_KEY
["*_PUBLIC*"] = "none", -- PUBLIC_KEY, MY_PUBLIC_VAR
["DB_*"] = "partial", -- DB_HOST, DB_PASSWORD
["DEBUG"] = "none", -- Exact match
}sources = {
[".env.local"] = "none",
[".env.production"] = "full",
[".env.*.local"] = "none",
}Priority: Key pattern → Source pattern → Default mode
shelter.nvim provides deep integration with ecolog.nvim, an LSP-powered environment variable manager.
- ecolog.nvim provides LSP features: completion, hover, go-to-definition, diagnostics
- shelter.nvim ensures values are never exposed, even in LSP responses
Without shelter.nvim, when you trigger completion or hover in ecolog, the actual values are visible. With the integration enabled, values are masked everywhere while still being functional.
Install both plugins:
-- lazy.nvim
{
"ph1losof/ecolog.nvim",
config = function()
require("ecolog").setup({
lsp = { backend = "auto" },
})
end,
},
{
"ph1losof/shelter.nvim",
config = function()
require("shelter").setup({
modules = {
files = true, -- Buffer masking
telescope_previewer = true,
ecolog = {
cmp = true, -- Mask completion values
peek = true, -- Mask hover content
picker = true, -- Mask picker entries
},
},
})
end,
},shelter.nvim intercepts ecolog-lsp responses at the LSP client level:
- Completion (
cmp): When you typeprocess.env., completion items show masked values - Hover (
peek): When you hover over a variable, the value is masked - Picker (
picker): The variable browser shows masked values
Copying/Peeking Values: Even with masking enabled, you can still copy the real value using ecolog's copy commands. shelter.nvim hooks into ecolog's on_variable_peek hook to provide the unmasked value when explicitly requested.
Toggle ecolog contexts independently:
local shelter = require("shelter")
-- Toggle all ecolog contexts
shelter.toggle("ecolog")
-- Toggle specific contexts
shelter.integrations.ecolog.toggle("cmp")
shelter.integrations.ecolog.toggle("peek")
shelter.integrations.ecolog.toggle("picker")local shelter = require("shelter")| Function | Description |
|---|---|
shelter.setup(opts) |
Initialize plugin |
shelter.is_enabled(module) |
Check if module is enabled |
shelter.toggle(module) |
Toggle module on/off |
shelter.get_config() |
Get current configuration |
shelter.peek() |
Reveal value while cursor is on it |
shelter.info() |
Show plugin status |
shelter.build() |
Rebuild native library |
shelter.register_mode(name, def) |
Register custom mode |
shelter.mask_value(value, opts) |
Mask a value directly |
| Feature | shelter.nvim | cloak.nvim | camouflage.nvim |
|---|---|---|---|
| Performance | 3-12x faster (Rust-native) | Pure Lua | Pure Lua + TreeSitter |
| Re-masking | Line-specific (incremental) | Full buffer re-parse | Full buffer re-parse |
| Partial masking | Built-in mode | Manual pattern workaround | Multiple styles (stars, dotted, scramble) |
| Multi-line values | Full support | Not supported | Supported |
| Quote handling | EDF compliant | Pattern-dependent | Basic |
| Preview support | Telescope, FZF, Snacks | Telescope only | Telescope, Snacks |
| Completion disable | nvim-cmp + blink-cmp | nvim-cmp only | nvim-cmp |
| Custom modes | Factory pattern | Lua patterns | Custom parsers |
| LSP integration | ecolog-plugin | None | None |
| Build step | Requires Rust | None | None |
| File types | Env files only | Any filetype | 13+ formats (env, json, yaml, toml, etc.) |
| Leak-free masking | Yes — zero-gap architecture | No — flashes values on open/paste | Debounce-based (may flash) |
| Security features | N/A | N/A | Have I Been Pwned checking |
Measured on GitHub Actions (Ubuntu, averaged over 10000 iterations):
| Lines | shelter.nvim | cloak.nvim | camouflage.nvim | Pure Lua | vs cloak | vs camouflage | vs Pure Lua |
|---|---|---|---|---|---|---|---|
| 10 | 0.01 ms | 0.04 ms | 0.08 ms | 0.02 ms | 4.0x faster | 7.4x faster | 1.6x faster |
| 50 | 0.06 ms | 0.18 ms | 0.37 ms | 0.10 ms | 3.1x faster | 6.2x faster | 1.8x faster |
| 100 | 0.11 ms | 0.37 ms | 0.70 ms | 0.20 ms | 3.3x faster | 6.3x faster | 1.8x faster |
| 500 | 0.50 ms | 1.77 ms | 3.35 ms | 1.08 ms | 3.5x faster | 6.6x faster | 2.1x faster |
| Lines | shelter.nvim | cloak.nvim | camouflage.nvim | Pure Lua | vs cloak | vs camouflage | vs Pure Lua |
|---|---|---|---|---|---|---|---|
| 10 | 0.01 ms | 0.04 ms | 0.08 ms | 0.02 ms | 5.6x faster | 10.5x faster | 2.5x faster |
| 50 | 0.03 ms | 0.18 ms | 0.36 ms | 0.11 ms | 6.5x faster | 12.8x faster | 3.9x faster |
| 100 | 0.04 ms | 0.36 ms | 0.71 ms | 0.20 ms | 8.7x faster | 17.0x faster | 4.8x faster |
| 500 | 0.20 ms | 1.84 ms | 3.31 ms | 1.07 ms | 9.3x faster | 16.8x faster | 5.5x faster |
| Lines | shelter.nvim | cloak.nvim | camouflage.nvim | Pure Lua | vs cloak | vs camouflage | vs Pure Lua |
|---|---|---|---|---|---|---|---|
| 10 | 0.02 ms | 0.05 ms | 0.09 ms | 0.02 ms | 2.6x faster | 5.1x faster | 1.2x faster |
| 50 | 0.03 ms | 0.19 ms | 0.39 ms | 0.10 ms | 5.8x faster | 12.2x faster | 3.0x faster |
| 100 | 0.07 ms | 0.35 ms | 0.73 ms | 0.21 ms | 5.1x faster | 10.7x faster | 3.0x faster |
| 500 | 0.33 ms | 1.75 ms | 3.46 ms | 1.24 ms | 5.2x faster | 10.4x faster | 3.7x faster |
Last updated: 2026-03-28
- Rust-Native Parsing — EDF parsing via LuaJIT FFI, no Lua pattern overhead
- Line-Specific Re-masking — Only affected lines are re-processed
- Zero Debounce — Instant updates with
nvim_buf_attach - Pre-computed Offsets — O(1) byte-to-line conversion
Unlike cloak.nvim and camouflage.nvim, shelter.nvim is architecturally designed to never expose sensitive values — not even for a single frame. cloak.nvim has an open issue where values are briefly flashed on file open and when pasting in insert mode. This happens because it relies on event-driven re-cloaking with inherent timing gaps.
shelter.nvim avoids this entirely:
- Synchronous
nvim_buf_attach— Masks are applied in theon_linescallback before Neovim renders the next frame, so changed lines are never displayed unmasked - Pre-populated cache — Initial buffer load parses and masks content before the buffer is displayed
- No debounce — Re-masking is instant and synchronous, not deferred via timers or
vim.schedule
This means shelter.nvim is safe for screen sharing, recordings, and any scenario where even a brief flash of a secret is unacceptable.
The benchmarks also include a Pure Lua baseline — simple Lua pattern matching with extmarks and full buffer parsing on every change. This represents the best you can physically achieve without a dedicated plugin or separate optimisations. Even this minimal approach is slower than shelter.nvim at scale because it still has to iterate every line in Lua and call into the Neovim API per match. Any future plugin that aims to match shelter.nvim's performance would need to move beyond pure Lua — either via a native binary, SIMD-accelerated parsing, or similarly complex incremental update strategies.
Choose shelter.nvim for dotenv files with maximum performance and features.
Choose cloak.nvim for any filetype with minimal setup.
Choose camouflage.nvim for multi-format support (JSON, YAML, TOML, etc.) with password breach checking.
┌─────────────────────────────────────────────────────────┐
│ shelter.nvim (Lua) │
├─────────────────────────────────────────────────────────┤
│ Config │ State │ Mode Factory │ Engine │ Integrations │
└─────────────────────────────────────────────────────────┘
│ LuaJIT FFI
▼
┌─────────────────────────────────────────────────────────┐
│ shelter-core (Rust cdylib) │
├─────────────────────────────────────────────────────────┤
│ EDF Parsing (korni) + Line Offsets │
└─────────────────────────────────────────────────────────┘
- Engine — Coordinates parsing, mode selection, and mask generation
- Mode Factory — Creates and manages masking mode instances
- Extmarks — Applies masks via Neovim's extmark API
- nvim_buf_attach — Tracks line changes for instant re-masking
- EDF — File format specification that korni uses to parse
.envfiles
- ecolog.nvim — LSP-powered environment variable management
- ecolog-lsp — The Language Server providing env var analysis
- ecolog-spec — EDF Specification
- korni — Zero-copy
.envfile parser (used internally)
MIT
