Repository navigation
v1.2.2
shim-cli 1.2.2
1.2.2 closes the ways 1.2.1 let content reach the model uninspected. An event
shim cannot inspect is now withheld or refused instead of passed through. A
tool's input is handled by where it goes, and an agent can no longer switch
shim off from inside the session shim guards. The hook protocol changed:
at several tool events the hook gives a different answer than 1.2.1 did, so
read "What to check after upgrading" before you upgrade a setup that scripts
around shim.
It started with an outside report. @madeinplutofabio found that one lone UTF-16
surrogate anywhere in a tool event turned masking off for the whole event, and
contributed the fix (GHSA-p6p2-3w76-pc9p). An outside review of the hook then
started an audit of every path where shim gave up and let content through.
This release fixes what that audit ranked first. What it found and 1.2.2 does
not fix is listed at the end, under "Known gaps, fixed in 1.2.3". Thank you to
both.
Upgrading is uv tool upgrade shim or pipx upgrade shim, or
/plugin update shim-cli@shim-cli in Claude Code; for VS Code, check out the
v1.2.2 tag in the folder you cloned. shim install writes the same entries
as in 1.2.1. The plugin's own hook files now pass the launcher a third
argument, prompt or call, and a plugin update brings them with it.
An event shim cannot inspect is no longer let through
In 1.2.1, a tool event shim could not inspect passed through unchanged with a
notice, so masking was off for that event. It also put a placeholder on the
fields it did reach and passed the rest through unread, for example the fields
past the 2,000th of an event, or past 24 levels of nesting. 1.2.2 fails closed
in every mode, observe included. That covers the 25-second deadline, an
analysis that fails, nesting deeper than 64 levels, an event over 1 MB, JSON
that does not parse, a settings file that is refused or invalid, and a shim
module that fails to import after an upgrade.
- A tool call is refused in Claude Code and VS Code:
shim: could not inspect this WebFetch call (settings unreadable), so it was not allowed. - A Claude Code tool result is withheld. An MCP tool's result becomes
[withheld by shim]. A built-in tool's result keeps its shape, with
[withheld by shim]in place of its text, because Claude Code sends the
model the original when a built-in result does not match the tool's output
schema. Two exceptions keep the session usable, because Claude Code sends
these to the API as more than text and the API would refuse a blanked one on
every later request:ToolSearchkeeps the tool names it found, which are
Claude Code's own and not content, and aReadof an image or a PDF is
withheld as a textRead. You seeshim: withheld this Read result; it could not be inspected (settings unreadable)., and the model is told the
result is not empty and to ask you how to continue. - Where the client allows neither (a failed call's output, any VS Code
result, or a built-in result over 1 MB whose shape never arrived), you and the
model are told it was not inspected, and the model is told not to repeat
values from it. - A field is never partly masked. An event read only in part is treated as
one not read at all. - The reason is one of four words:
deadline,too large,
settings unreadableoranalysis failed. No message or record carries text
from the event.
A refused or invalid settings file used to block every prompt but let all tool
traffic through uninspected. Now tool calls are refused and results withheld
too. A settings file read while shim config is saving it is read again, up to
three times, instead of being refused. With standard error closed, the hook
used to block every prompt and pass every tool result unmasked; it now inspects
both.
The session summary says what happened to each uninspected event:
skipped 2 not inspected, withheld or refused (Read .env) when shim stopped
it, and not inspected, passed through when the model has it anyway. The
cli.session.not_inspected finding is now titled "Tool events shim could not
inspect" and no longer says results pass through unchanged.
A tool's input is handled by where it goes
1.2.1 masked the input of every tool that was not a command or a file write. A
Read in a folder named after an email address, as macOS names a Google Drive
folder, was rewritten to /<EMAIL_1>/plan.txt and failed. An MCP tool was
handed placeholders, so an MCP file write could put <SECRET_1> into a file.
- Local tools are only reported. The input of
Read,Grep,Glob, a
sub-agent's task, a question to you and the like (VS Code:read_file,
grep_search,list_dirand the like) is never rewritten or refused. A
finding there showsshim: found EMAIL (1) in Read. Not modified.under the
newlocal-tooldirection, whose default iswarn;local-tool = "observe"
silences it. The answer you type toAskUserQuestionislocal-tooltoo,
so it is no longer masked as a tool result. - Web tools are masked, and you are told.
WebFetchandWebSearch
arguments are masked as before, and you now see
shim: masked EMAIL (1) in WebFetch. - MCP tools and tools shim does not know are never rewritten. Under the
defaultenforce, a call whose arguments carry a finding is refused, with
shim cannot tell where this tool sends its input, so it refuses the call rather than rewrite it.outbound = "warn", or a key for one tool such as
"mcp__crm__lookup" = "warn"under[mode], reports it instead. - Tool names are per client.
Monitorandmcp__ide__executeCodeare
commands in Claude Code, so their input is no longer rewritten into a
different command. In VS Code,run_in_terminal,send_to_terminal,
create_and_run_taskandrun_playwright_codeareexecutable-text, and
the file-editing tools arelocal-write, so those two keys now apply there. - The diet reshapes only web and MCP tool results. Command output,
curl
throughBashincluded, and local tools' results are never reshaped, because
either may be a file the model goes on to edit. A reshaped result now carries
one sentence for the model:shim: reformatted this result to save space (json); it is not byte-for-byte what the tool returned.
Tool results are masked as before. A path that holds an email address in a
result, such as a Glob file name, still comes back with a placeholder (see
"Known gaps").
An agent cannot switch shim off from inside its session
An agent could rewrite shim's settings, or run shim config, and turn
detection off without a trace. In every mode, and whatever else it finds, the
hook now refuses:
- an agent's write, through a file tool, to shim's settings file, to a client
settings file that holds or switches on shim's hooks
(~/.claude/settings.json,~/.codex/hooks.jsonandconfig.toml,
~/.copilot/hooks/shim.json), or to any file in the plugin folder running
the hook; - an agent's shell command that runs
shimwithconfig,revertor
uninstall, everyshim configincluded, also after;,&&or a pipe and
inside$( ).
The refusal names only the file or the subcommand and tells you to do it
yourself in your own terminal. Other routes are not caught: the shell writing
the file (sed -i, >), an interpreter, sh -c, sudo, a variable holding
the program name, uv tool uninstall shim, a project's disableAllHooks, and
VS Code's chat.pluginLocations, among others.
Privacy lists them all; for those
the client's permission prompt is still the guard.
If you develop shim-cli with your working checkout as the active plugin
folder, agent edits inside it are refused: point the plugin at a separate
clone.
The detector keeps up with what it accepts
The deadline used to arrive before the detector finished on ordinary large
output, and the event then passed through unmasked.
- The email, JWT, connection-string and base64 scans run in linear time. Some
40 KB inputs used to take 9 to 10 seconds. - The phone scan is about ten times faster on numeric text, and text without a
digit no longer loads the phone library at all. A number deep in a long table
is still checked: the library's limit of 65,535 tries per text no longer
applies. - Most text up to the 1 MB input bound is inspected well inside the deadline.
Measured on a loaded M1: about 2.5 s for a 1 MB numeric CSV, 3 s for base64,
1 s for e-mail punctuation and 5 s for a list of IP addresses. Not all of
it: long lines dense with short numbers still reach the deadline (see "Known
gaps"), and are then handled as an event shim could not inspect, never
masked in part. - Expanding Unicode no longer switches detection off for a field. A run of
U+FDFA, which expands eighteen-fold, used to leave the whole field it was in
unmasked; such text is now read in pieces of at most 11,111 characters, at
about 3 s per 100,000 characters. A run of combining marks is normalised in
slices of 30, so 100,000 marks no longer take 6 s. - A JWT-shaped token with more dotted segments may be masked slightly wider
than before. Nothing that was masked is left unmasked. - The latency gate is back. CI and the release fail when a hook median passes
420 ms, or 2,000 ms for a prompt built from the shapes that used to make the
detector quadratic, which now takes about 0.2 s.
The plugin launcher
- No usable Python no longer means no guard. With no Python that can run
the hook, the launcher used to let the prompt through, even under
user-prompt = "enforce". It now withholds the prompt with the hook's own
answer, refuses a tool call, and tells you any other event was not
inspected, with one line on stderr such asshim: no python3 found on PATH; the prompt was withheld.A Python older than 3.9 does the same. - An interpreter that cannot start is skipped. A broken
shim-hookon
PATH(a virtual environment that lost shim, an interpreter that is gone, a
first line that names no Python) used to switch the bundled archive off. The
launcher now falls through to the archive, and through archive candidates
that cannot start, such as a pyenv shim whose version is missing or the macOS
xcode-selectstub. - Python runs isolated. The archive runs with
-Iand the package script
with-E, soPYTHONPATH=.and a project file namedtoken.pyor
types.pycan no longer run inside the hook and switch masking off.
Installing, the CLI and shim watch
shim installchecks the hook it writes. Before writing, and on
--dry-run, it runs the exact command it registers on a synthetic safe and
a synthetic sensitive prompt. A shim installed withpip install --userused
to install and passshim statuswhile its-Ihook could not import shim
and let every event through. Nowinstallrefuses withDETECTOR_UNAVAILABLE
and exit2and writes nothing, andshim statusfails the same way.
Install withuv toolorpipx.- One shim per client. A second installation, such as pipx beside
uv tool, used to add a second full hook set, andshim revertthen left one
running while saying it was removed. Now install replaces another
installation's hook and says so, revert removes every shim hook from any
installation, andshim statusandshim doctorname the other
installation. A hook that wraps shim's command (| tee,&& notify) is
yours and left alone.shim install copilotno longer refuses ashim.json
that another installation wrote.installanddoctorare fast on large
settings files again: on a 720 KB file, doctor took 0.35 s instead of 42 s. shim configno longer drops settings without a word. On a settings
file that did not parse,--onlyused to write a new one, discarding its
[mode](auser-prompt = "enforce"among it),[entities],[[custom]],
[reveal], the ledger andmarkers. It now fails withSETTINGS_INVALID
like every other flag and writes nothing. A pattern added beside--only
turnsCUSTOMon, where it used to stay off.--resetused to ignore
--custom,--reveal,--ledgerand--dietgiven with it; it now refuses
every other option. On a refused file,--resetand
--onlyreportSETTINGS_REFUSEDwith thechmodfix instead of
SETTINGS_CHANGEDandRun the command again.shim redactwrites UTF-8 to a file or pipe whatever the locale. Under
tr_TR.ISO8859-9it used to transcode the file silently, and under Latin-1
it crashed.shim watchcounts with your settings. A type you disabled is no longer
counted, and an opt-in type you enabled and your custom patterns are. A
settings file it cannot read stops it before the client starts, with
SETTINGS_INVALIDorSETTINGS_REFUSED.shim watchno longer leaves the client behind.SIGTERMandSIGHUP
reach the client, Ctrl-C reaches it once and escalates on repeats, and the
proxy keeps its port until the client has exited, so no other process can
take the port while the client still sends credentials to it. A client killed
by a signal exits128plus the signal number, and--jsonsays
"status": "signalled".shim watchon more networks, and more honest. The proxy follows
HTTPS_PROXY(http://proxies) andNO_PROXY, and usesSSL_CERT_FILEfor
a custom CA. Repeated request headers are forwarded as sent. A response of an
unexpected shape is relayed whole instead of cut off. A field named
signatureis counted unless it is a thinking block's. A request read only in
part, or still being measured when the report is written, says so. The section
split never adds up to more than the request. WithANTHROPIC_BASE_URLset,
the refusal no longer tells you to unset it, which would send that endpoint's
credential toapi.anthropic.com; its fix is nowshim install claude.shim watchstarts without a DNS lookup. Starting the proxy looked up
the name of127.0.0.1, which on a machine with a slow or unreachable DNS
resolver heldshim watchbefore the client started, for more than 20
seconds on GitHub's macOS runners. The proxy now only binds its port.
What to check after upgrading
- MCP tools in Claude Code: a call whose arguments carry a finding is now
refused by default. Setoutbound = "warn", or"<tool name>" = "warn"
for one tool, under[mode]to report it instead. - VS Code refusals: if you set
outbound = "enforce"to refuse terminal
commands, setexecutable-text = "enforce"now; file edits are
local-write.outboundcoversfetch_webpage, MCP and unknown tools. A
local tool such asread_fileis never refused. - A per-tool key on a local tool, such as
Read = "enforce", now reports
instead of masking. - Projects whose path holds an email address get a
found EMAIL ... Not modified.line on eachRead,GloborGrep;
local-tool = "observe"silences it. - Tool events fail closed in
observetoo, and a VS Code call shim cannot
inspect is refused under the shipped defaults. AWriteorEditover 1 MB
is refused. - The
htmldiet no longer converts command output. A page youcurl
throughBashreaches the model as markup; an MCP or web tool's page is
still converted. - Scripts that read output:
--resetwith another option now exits2
withOPTIONS_CONFLICTand the message--reset cannot be combined with other options.;shim statusexits2withDETECTOR_UNAVAILABLEwhen the
installed hook cannot run;BASE_URL_ALREADY_SET'sfixisshim install claude; the
cli.session.not_inspected,cli.doctor.hook_not_installedand
cli.doctor.duplicate_hookstexts changed. Matchcode, never the text. - Downgrading: 1.2.1 reads a 1.2.2 settings file and ignores a
local-toolkey, but passes an event it cannot inspect through again, and
its launcher lets a prompt through when no Python can run the hook.
Known gaps, fixed in 1.2.3
These were found by the same audit and are not fixed in 1.2.2. Each already
existed in 1.2.1.
- Number-dense text can still reach the deadline. The phone scan reads a
long line of short numbers, such as- 0 - 0 - 0repeated, at about 6 s per
100,000 characters on an M1, so a field of a few hundred thousand such
characters reaches the 25-second deadline. It is then handled as any event
shim could not inspect: withheld or refused, or, where shim can only report
(a failed call's output, a VS Code result, a file attached with@),
reported as not inspected while the model has it. The same text broken into
lines is read in well under a second. 1.2.1 was slower still on it, and let
the event through unmasked at the deadline.
A list of phone numbers is read faster when numbers repeat, as they do in
logs and exports: a repeated number is checked once. A megabyte of distinct
valid numbers still takes about 5 s on an M1, each one checked by the phone
library; it is inspected whole, inside the deadline. - Placeholder numbering (per-event numbering, single HTML scan). Numbers
restart for every field and event, so two different addresses can both be
<EMAIL_1>and one value can get several numbers. Withdiet = ["json", "html"], an IBAN split across two<span>s is masked only in part. Line
numbers after a masked multi-line value, such as a PEM key, are off. A
[reveal]tail can show the wrong digits for a percent-encoded value. Text
shim already masked can be counted again as a secret the model wrote. - Keys and numbers in structured payloads. A JSON object key is never
inspected, so an email address used as a key passes. A card, national ID or
phone number sent as a JSON number passes. A secret under a structured key,
such as{"password": "..."}or a{"name": "DB_PASSWORD", "value": "..."}
pair, can pass unmasked. A PDF or imageReadis scanned as text and can be
rewritten whole. - Email addresses that swallow a path. In a tool result,
/work/x@example.com/notes.txtbecomes/<EMAIL_1>/notes.txt, a Google
Drive folder named after an address hides the whole path, and an SSH remote
git@github.com:org/repo.gitis masked as an address. - Placeholders written back to disk. After a masked
Read, the model's
WriteorEditcan put<SECRET_1>over the real value in the file, with
no warning, even underlocal-write = "enforce". - Secret values in attributes and escaped JSON. In
<add key="DbPassword" value="..."/>the placeholder lands on the attribute
name and the value stays in clear, while shim reports it masked. A masked
value swallows what follows a;,&or), such as connection-string
options. A password inside escaped JSON, as AWS Secrets Manager prints it,
passes in clear. - Session records. On an event shim could not inspect, and for a file
attached with@, a value that only a custom pattern or an opt-in type
matches is written into the record's file path as it is. A[reveal]tail
reaches the record and the ledger. A value split by an invisible character
is joined back together in the record.[entities]keyed by
UserPromptSubmitis ignored for prompts. - Secret key names. Keys with the secret word glued on (
PGPASSWORD,
SECRETKEY,ACCESSTOKEN) and typed declarations
(db_password: str = "...") are not detected. Nor is a value away from its
key: Maven<password>, Kubernetesname:/value:lists, AWS SSM, and
Terraformdefault =. A digit-only password underDB_PASSWORDor
REDIS_PASSWORDpasses. - Dropped records. Parallel hook events can drop session and ledger
records without a word, so a summary undercounts. The ledger stops at its 5
MB monthly cap silently. A ledger folder others can read drops every record,
and theLEDGER_UNREADABLEfix points atshim doctor, which has no ledger
check. - Connection strings with other schemes. The user and password in
postgresql+asyncpg://,mysql+pymysql://,mariadb://,amqp://,
smtp://orssh://URIs pass in clear. - Context cues in code style. A Turkish tax ID after
TAX_ID=,taxId,
tax_numberorvergiKimlikNois missed. So is a phone number after
phoneNumber,mobileNumberorcontactPhone, or in a vCard
TEL;TYPE=CELL:line. - Diagnostics that say the wrong thing. Some refusals name the wrong
reason. A hook group where shim's hook sits beside another hook is refused
with a fix asking you to make a file parse that already parses, and a
group-writable file is blamed on another user. Doctor ignores
CLAUDE_CONFIG_DIRwhen it looks for the plugin, counts disabled plugins,
and crashes when two shim plugin keys are installed. A failed 0.2.0 settings
move prints a traceback.[mode] default = "warn"silently turns off
tool-result masking. Custom patterns stay listed whileCUSTOMis off.
SHIM_CONFIGnaming a missing file silently runs the defaults. - Pattern coverage. PGP and DSA private key blocks, Stripe restricted keys
(rk_live_), Slack app tokens (xapp-), GitLab runner and deploy tokens,
Proxy-Authorization: Basic, an undelimited 9-digit SSN afterssn, and
18- or 19-digit cards are not detected. An address that is only an address
before percent-decoding is missed. A Turkish instruction-override line that
starts like a commit subject (fix:) is not flagged.shim configaccepts
some custom patterns that backtrack, or that only backtrack with
ignore_case; the hook then hits its deadline and withholds the result. shim auditandshim keys.shim audit --purgewith a linked
history.jsonldeletes the transcripts but keeps the prompt lines.shim auditsayssafeand exits0when it skipped lines.shim keys --jsonon
an unreadable file prints a usage error instead of a JSON error.