-
Notifications
You must be signed in to change notification settings - Fork 0
Security Model
camouflage.nvim hides sensitive values visually by drawing over buffer text with Neovim extmarks. It does not modify, encrypt, delete, or sandbox the real file contents.
camouflage is designed for casual on-screen exposure:
- screen sharing
- pair programming
- demos and recordings
- shoulder-surfing
- screenshots of the editor window
The original text remains in the buffer and on disk.
A value is covered before the frame that would show it is drawn. The changed
rows are masked inside the buffer change itself, from a line-level match, and
the exact masks follow once you stop typing. That covers typing a new value,
appending to one, putting a line with p, and a bracketed paste.
The same applies to places that show a file's text without being the file:
- Telescope, Snacks, fzf-lua and mini.pick previews
- picker result rows in the same four pickers, where a grep hit carries the matched line (and mini.pick's preview title, which repeats it)
- quickfix and location list rows
- diffs, including the one
git commit -vputs under the message -
:terminaloutput, if you turn Terminal Masking on
Presentation Mode turns the lot on for the length of a demo and refuses reveal, follow-cursor and toggle while it is on.
Anything that reads the buffer or file contents directly can still see the real values:
- grep and search tools outside the editor, and any picker camouflage has no integration for
- LSP servers, completion sources, formatters, linters, and AI assistants (nvim-cmp and blink.cmp are turned off in masked buffers, the rest are not)
-
:%print, command output, substitute previews, and custom scripts - normal yanks such as
yy,"+y, or:%y -
:registers, which prints what a register holds, including a value copied withyy - the message Neovim prints when it jumps to a quickfix or location list entry,
(1 of 3): API_KEY=...after:vimgrep,:cfirst,:cnextand the like. The list window is masked, but the message isn't a buffer, so there is nothing to draw over.:vimgrep /pattern/jfills the list without jumping, and:silent cfirst/:silent cnextjump without the message.:grepalso prints the external command's output while it runs - saved files, backups, swap files, undo files, shell history, and git history
Use :CamouflageYank when you intentionally need to copy a real value. It can prompt for confirmation and auto-clear the configured register. A clear still pending when Neovim exits runs on the way out, before ShaDa is written.
:CamouflageRegisters lists the registers the way :registers does, with the
contents of any register holding a masked value replaced. The registers
themselves are untouched, so pasting still works.
stars, dotted, and text are safer choices for screen sharing because they do not reveal the original characters.
scramble is cosmetic, not protective. It shuffles the original characters, so it leaks the value length and character set.
partial reveals what it is asked to reveal. It is reachable per key through a
Rule Based Policy rule, and it belongs on identifiers such as an access key
id rather than on passwords.
Every style keeps the value's width, so the length is visible. mask_length
fixes the mask width and hides that too.
.camouflage.yaml is data-only. camouflage checks each option against the type of its default and drops one that doesn't fit, with a warning. A project file can't register Lua functions or run project code, can't set shield, and can't turn on HIBP network checks unless it is trusted.
A project file that turns masking off gets a warning with its path. So does one that can leave values unmasked another way: auto_enable: false, a max_lines below the default, policy.enabled: false, policy.default_action: ignore, or a policy rule or terminal path ignore that matches every value.
If you work with untrusted repositories, enable Neovim's trust gate:
require('camouflage').setup({
project_config = {
secure = true,
},
})With secure = true, a project config is not applied until Neovim trusts the file through vim.secure / :trust.
Built-in local checks such as Weak Secret Check and JWT Expiry Hints run without network access.
Have I Been Pwned sends only the first five characters of a SHA-1 hash prefix to the HIBP range API and compares suffixes locally. The plaintext value is not sent.
Custom Check API functions are trusted Lua code. They receive plaintext values through ctx.var.value, so do not register checks from untrusted project files or third-party snippets you have not reviewed.
Workspace Audit output is redacted. Findings include key names, locations, parser names, value lengths, and policy metadata, but not plaintext values. Audit does not run hooks or network checks.
:checkhealth camouflage reports whether masking is on, which parser handles the
buffer you were in, how many values it masks, which grammars are installed, and
whether a project config was found and trusted. No value is printed in it.
- Presentation Mode: one command for "I am about to share my screen"
- Terminal Masking: opt-in masking for command output
- Workspace Audit: redacted workspace scanning
- Rule Based Policy: data-only masking policy
- Custom Check API: trust boundaries for executable checks