Skip to content

Security: NMenzel/claude-devtools-mod

docs/SECURITY.md

Security and privacy

Claude DevTools is local-first. It makes no network calls and no model calls, and it sends no telemetry. It calls none of $.http, $.model or $.process. Error Lens diagnoses failures with fixed rules, never a model. Run claude plugin validate . to confirm the full list of calls it makes.

What it can and cannot do to a tool call

  • It never approves anything. Continue calls next(e). Claude Code's permission rules, settings hooks, sandbox and the permission prompt still decide afterwards. DevTools has no tool.check hook that changes a verdict; its only tool.check hook observes.
  • It never runs a call after you reject it. Reject, a dismissed dialog, an interrupted turn and a headless pause all return { deny } without calling next.
  • It never runs a call twice. The hook calls next once. Its .catch handler replays a settled result instead of running again.
  • It fails closed where it promised to hold. If its own hook fails before deciding, a call a pause breakpoint would hold is refused. Other calls proceed.
  • It does not alter results. The value next(e) returned is handed back unchanged. Only an explicit Simulate replaces a result, and that is opt-in, allowlisted and labeled.
  • It cannot weaken managed policy. Managed PreToolUse hooks run before any mod. Organization mods (prependPlugins) sit outside it. It never changes a setting. For the Permissions tab it only reads the permissions of each settings file ($.settings.read): rules, default mode, extra directories. Rules are redacted like everything else it keeps.
  • It only reads tool results. Its session.append hook, matched to tool results alone, passes every row on as it is. It keeps the refusal text of a call refused before DevTools saw it, redacted and cut to 600 characters, and reads that one call's input from the conversation ($.session.messages), kept as the same redacted summary the timeline keeps.
  • It answers only you. Every command is refused unless you started it: typed, from Remote Control, from the SDK host, your own Slack ping or a follow-up to your own action in the UI. A run Claude Code cannot attribute (unclassified) is refused too; the prompt and the pane always work. Another session, a Slack or Telegram channel, a schedule or another mod cannot switch off the breakpoints you set, open the pane under your typing, or read your rules and refused calls out.
  • Summaries are redacted by key too. A tool with no summary of its own (an MCP tool) is summarized from its arguments with password, token, api_key and the like dropped, as its stored input is.
  • Errors are redacted before they are cut. A settings file that cannot be read is named with its error, redacted first: a parse error can quote the file; it is cut at 4 KB before, and 200 characters after. A long command or error is cut at 64 KB before redaction, dropping the word the cut splits.

The mod is not a sandbox. It is a debugger. Tool-name and path matching are best effort. Shell commands are matched as text without full shell parsing, and a path can be spelled in ways a glob does not foresee (symlinks, .., environment variables). Use permission rules, sandboxing and managed settings for enforcement.

Error Lens

  • It is passive. It never retries a call, changes a result, or interrupts. A failed call returns after a few in-memory state writes, the same cost as recording it, and the file checks run after the result has gone back to Claude.
  • Its checks are read-only metadata: one $.fs.stat per path (exists, kind, size, modification time, link target). It never reads, opens for writing, creates or deletes a file. At most 5 paths per failure: the target, its parent folder and paths the error message names. Network (\\server\share) paths, globs and redacted or truncated spellings are never checked. Set errorLens to classify for no file system access at all.
  • What it keeps follows the timeline's rules: the error text and the arguments are redacted (with redaction on), file contents are omitted (a Write keeps only its byte count), and the error is truncated to 3,000 characters. Records live in $.state for the session (at most 80) and appear in exports.

What it records

By default every event keeps a summary:

  • tool name, ids, agent, timestamps, duration, status and outcome, risk label, permission verdict, breakpoint and decision
  • an input summary, plus the arguments as JSON with:
    • file contents omitted: Write.content, Edit.old_string and new_string, NotebookEdit.new_source become <N chars omitted>
    • strings truncated to maxSummaryChars × 4
  • a result summary that never holds output contents: line and character counts, numFiles, an interrupted flag. Error text is kept (redacted and truncated) because it is the point of debugging.

Redaction (on by default) applies to every summary, input, path, error and export. It covers:

  • private keys, Bearer and Basic credentials, Anthropic/OpenAI-style sk- keys, GitHub tokens (ghp_, github_pat_), GitLab, Slack, AWS access keys, Google API keys, npm tokens and JWTs
  • passwords in URLs (scheme://user:[REDACTED]@host)
  • every inline environment assignment value (NAME=…, export NAME=…, PowerShell $env:NAME = …), whatever the variable is called
  • secret CLI flags (--password, --token, --api-key, …) and secret headers
  • object keys named like secrets (password, apiKey, client_secret, Authorization, cookie, …)

Redaction is pattern-based. It reduces exposure, and it cannot guarantee a string holds no secret.

Raw capture (opt-in, off by default)

Setting captureRaw also keeps the raw input and result text, truncated to 20,000 characters and still redacted unless redaction is off. This can capture file contents and command output. The pane, /devtools-status and the exports all show that raw capture is on.

Where data lives

Data Location Lifetime
Timeline, pending calls, counters, Error Lens records, refused calls (at most 80), the permission rules last read $.state (host memory) the session; lost on exit and on /clear
Breakpoint rules and mode (no hit counts, no events) $.store, a JSON file of the plugin's own under your Claude Code config directory until you clear it (/devtools-break clear) or turn off persistBreakpoints
Exports only where you run /devtools-export. The default is .claude-devtools/ in the working directory, so add it to .gitignore until you delete them

Export paths are validated: only .json or .md names, no .. segments, no control characters, no ~ or environment variable (nothing expands them, so they would make a literal ~ folder), no hidden file or folder other than .claude-devtools/ (so no .claude/, .mcp.json or .vscode/), and no instructions file (CLAUDE.md, CLAUDE.local.md, AGENTS.md, GEMINI.md). Outside .claude-devtools/ an export never replaces an existing file ($.fs.exists first), so it cannot overwrite a build, start-up, settings or instructions file. A relative path resolves under the session's working directory.

Patterns as input

Rules come from you, but stored rules are still re-validated when they load. Matchers are bounded so a rule cannot stall a tool call:

  • word patterns match by indexOf, not regex, and scan at most 8 KB
  • re: regexes: at most 200 characters, no backreferences, no quantified groups ((a+)+), at most 3 unbounded quantifiers, and at most 2 KB of input scanned
  • globs: at most 512 characters and 12 wildcards, paths at most 1 KB

Reporting a vulnerability

If you find a way Claude DevTools could leak data or weaken a permission decision, for example a credential format the redaction misses or a call that runs after a Reject, please report it privately. Use GitHub's Security → Report a vulnerability on this repository, not a public issue.

There aren't any published security advisories