Skip to content

Workspace Audit

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

Workspace Audit

Workspace audit scans supported files under a project root or explicit path and writes redacted findings to quickfix or the current window's location-list.

It uses the same parser registry as live masking, including built-in parsers, runtime parser registrations, and custom patterns. It does not create buffers, apply extmarks, run hooks, or run network checks.

Commands

Command Destination Description
:CamouflageAudit audit.destination or quickfix Scan the current project root
:CamouflageAudit {path} audit.destination or quickfix Scan a specific file or directory
:CamouflageAudit! location-list Scan the current project root into the current window's location-list
:CamouflageAudit! {path} location-list Scan a specific path into the current window's location-list

If no path is provided, the root is resolved from the nearest .camouflage.yaml, then the nearest .git directory, then the current working directory.

Privacy

Audit output never includes plaintext values. Findings include:

  • file path
  • line and column
  • parser name
  • key name
  • nested/commented/multiline metadata
  • value length
  • the policy decision: action, reason (default, rule, policy_disabled) and the rule_id when a rule made it

The list rows read like this, here with a rule force-secrets that masks password keys:

.env|1 col 9-21 info| [env] API_KEY · 12 chars · mask
.env|2 col 7-11 info| [env] DEBUG · 4 chars · mask
config.json|2 col 30-44 info| [json] database.password · 14 chars · mask (rule force-secrets)

The rule id shows when a policy rule decided the row. Each item also carries the full record, the same one the JSON report has, as user_data (Neovim 0.10+, since 0.9 has no user_data on quickfix items):

local item = vim.fn.getqflist({ items = 0 }).items[1]
print(item.user_data.key, item.user_data.value_length, item.user_data.policy.action)

The audit engine does not run Have I Been Pwned or any other network-backed check. It also does not run variable_detected hooks, so hook code cannot inspect audit values.

Configuration

require('camouflage').setup({
  audit = {
    ignore_patterns = { '.git', '.git/**', 'node_modules', 'node_modules/**' },
    max_files_per_chunk = 50,
    destination = 'quickfix', -- 'quickfix' | 'loclist'
    open = true,
    notify = true,
  },
})
Option Default Description
ignore_patterns {'.git', '.git/**', 'node_modules', 'node_modules/**'} Root-relative or basename globs skipped by audit
max_files_per_chunk 50 Files processed per scheduled async chunk
destination 'quickfix' Default list target for :CamouflageAudit
open true Open quickfix/location-list when findings exist
notify true Show completion notifications

ignore_patterns are simple globs, not full .gitignore semantics. Use explicit directory patterns such as dist/** or .terraform/**.

Policy Interaction

Audit applies Rule Based Policy before reporting findings. Ignored variables are not listed, and audit stats include how many values policy ignored.

When policy metadata is attached to a finding, it is redacted:

{
  action = 'mask',
  reason = 'rule',
  rule_id = 'force-client-secrets',
}

Machine Readable Output

For a CI step or a pre-commit hook, the same records can be written as JSON, with an exit code that says whether anything was found:

nvim --headless -c 'CamouflageAudit --json=audit.json --quit .'
Flag Effect
--json={path} Write the report to a file
--json Write the report to stdout
--quit Leave Neovim afterwards: exit 1 with findings, 0 without

Without either flag the command behaves as before and opens a list.

{
  "version": 1,
  "plugin_version": "0.17.2",
  "root": "/tmp/demo",
  "cancelled": false,
  "parsers": ["dockerfile", "env", "hcl", "http", "json", "netrc", "properties", "toml", "xml", "yaml"],
  "stats": {
    "findings": 3, "files_seen": 2, "files_supported": 2, "files_scanned": 2,
    "files_skipped": 0, "policy_ignored": 0, "elapsed_ms": 0
  },
  "findings": [
    {
      "filename": "/tmp/demo/.env",
      "lnum": 1, "col": 9, "end_col": 21,
      "key": "API_KEY", "parser": "env",
      "value_length": 12,
      "is_nested": false, "is_commented": false, "is_multiline": false,
      "policy": { "action": "mask", "reason": "default" }
    }
  ],
  "errors": []
}

Only the first finding is shown here. The keys come out in whatever order vim.json.encode gives them.

No value appears anywhere in the report, the same rule the list output follows.

Lua API

The audit module is available for advanced workflows:

local audit = require('camouflage.audit')

local result = audit.run({
  path = vim.fn.getcwd(),
})

audit.set_list(result, {
  destination = 'quickfix',
  open = true,
})

For async scanning:

local handle = require('camouflage.audit').run({
  path = vim.fn.getcwd(),
  async = true,
  on_complete = function(result)
    require('camouflage.audit').set_list(result)
  end,
})

-- Optional cancellation
handle.cancel()

The report is available directly:

local audit = require('camouflage.audit')
local result = audit.run({ path = vim.fn.getcwd() })

local report = audit.to_report(result)   -- table, ready to encode
audit.write_report(result, 'audit.json') -- returns ok, err
audit.write_report(result, '-')          -- stdout

See Also

Clone this wiki locally