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.
- 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 notool.checkhook that changes a verdict; its onlytool.checkhook 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 callingnext. - It never runs a call twice. The hook calls
nextonce. Its.catchhandler 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
PreToolUsehooks run before any mod. Organization mods (prependPlugins) sit outside it. It never changes a setting. For the Permissions tab it only reads thepermissionsof 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.appendhook, 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_keyand 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.
- 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.statper 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. SeterrorLenstoclassifyfor 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
$.statefor the session (at most 80) and appear in exports.
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_stringandnew_string,NotebookEdit.new_sourcebecome<N chars omitted> - strings truncated to
maxSummaryChars × 4
- file contents omitted:
- 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,
BearerandBasiccredentials, Anthropic/OpenAI-stylesk-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.
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.
| 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.
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
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.