Skip to content

Security Model

Ahmet Zeybek edited this page Sep 27, 2026 · 3 revisions

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.

What It Protects Against

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.

While you edit

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.

Beyond the file buffer

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 -v puts under the message
  • :terminal output, 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.

What It Does Not Protect Against

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 with yy
  • the message Neovim prints when it jumps to a quickfix or location list entry, (1 of 3): API_KEY=... after :vimgrep, :cfirst, :cnext and the like. The list window is masked, but the message isn't a buffer, so there is nothing to draw over. :vimgrep /pattern/j fills the list without jumping, and :silent cfirst / :silent cnext jump without the message. :grep also 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.

Mask Styles

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.

Project Config

.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.

Checks

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.

Audit

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.

Health Check

: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.

See Also

Clone this wiki locally