Repository navigation
Releases: GetSHIM/shim-cli
Release list
v1.2.3
shim-cli 1.2.3
1.2.3 closes most of the gaps the 1.2.2 notes listed under "Known gaps, fixed
in 1.2.3". A placeholder the model writes back into a file is now refused, a
value keeps one placeholder number across a whole event, keys and numbers in a
tool's JSON are inspected, and the detector reads more secret shapes while
masking exactly the value. Session records no longer drop or keep what they
should not, and the hook answers before the client stops waiting, even on a
starved machine. Three of the listed gaps are fixed only in part: the pattern
coverage batch is in but for an address split by an encoded space, MCP calls
are refused on less detector noise but still on code that reads like a
password, and detector throughput is fixed for @ mentions and the
memory of shim watch but not for one line of number-dense text. What each
still costs is under "Known gaps" at the end.
The hook protocol changed again in places: a write can now be refused in
every mode, a tool result can be withheld after it was inspected, and a few
messages and exit codes changed. Read "What to check after upgrading" before
you upgrade a setup that scripts around shim.
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.3 tag in the folder you cloned. shim install writes the same entries
as in 1.2.2.
A placeholder is never written back
In 1.2.2, after a masked Read the model held <SECRET_1> where the file held
the key, and a Write or Edit of what it saw put the placeholder over the
real value on disk, with no warning, even under local-write = "enforce".
- The write is refused, in every mode. A
Write,Edit,MultiEditor
NotebookEdit, a VS Code edit tool (forapply_patch, its added lines), or
any MCP or unknown tool call whose new text holds a placeholder shim issued
in this session is denied:shim: this Write call would write <SECRET_1>, a placeholder shim gave you in place of a real value, instead of that value. Change the real value another way, or ask the user.The text an edit
replaces is not new text, so an edit that removes a placeholder goes through. - Only placeholders shim issued count.
<EMAIL_1>in a doc or a test
fixture, where shim never issued it in this session, is written as it is,
and so is the same write in another session. To tell the two apart, shim
keeps the names it issued beside the session record: a type and a range of
numbers per line, such asEMAIL 1 3, never a value. A masked tool result,
the redacted copy of a prompt blocked underenforceand the prompt Copilot
is given in place of yours all issue names. The store has its own cap, so a
full session log cannot drop it, andSessionEnddeletes it with the log. - It fails closed. If the names cannot be read, a write holding
placeholder-shaped text is refused; if they cannot be kept, a masked result
is not handed on. - Shell commands are not read for placeholders, and Codex and GitHub Copilot
CLI send shim no tool event, so there is no write to refuse there. The
session summary does not list these refusals; the record saysdenywith
the noteplaceholder.
The model is also told the truth about placeholders. Its note on a masked
result ended the source does not contain placeholders. even when the source
did; that clause is now said only when the event holds no placeholder-shaped
text at all, and shim's own numbers start above any it holds.
Placeholder numbers hold for a whole event
- One value, one number. Within one prompt or one tool event a value
always gets the same number and two values never share one, across every
field and the converted page. Before, numbers restarted for every field, so
two people could both be<EMAIL_1>, and one value repeated could become
<EMAIL_1>,<EMAIL_2>and<EMAIL_3>. One account, card or phone written
with different spacing, hyphens or case is one value. Numbers can skip but
never repeat, and they start again with the next event: the numbering lives
in memory for that event only, and no value or fingerprint is written down. - Line numbers stay true. A masked span that covered line breaks, such as
a PEM key or a wrapped base64 block, keeps them after its placeholder, so the
line numbers of aReadafter it are right. - The
htmldiet scans the page once, as the model reads it. A page is
converted first and then scanned, so an IBAN split across two<span>s is
masked whole; in 1.2.2 its first groups reached the model. A value that runs
across tags (password=<b>...</b>) is masked in the markup before
conversion. A page that cannot be converted, or whose text cannot be scanned
in full, is handed on as masked markup, never converted. [reveal]shows the right digits. A tail is read from the decoded value
alone, in ASCII digits, so a percent-encoded IBAN shows its own last digits
and a card beside a phone number never shows the phone's.- Text shim already masked is not counted again by the
Stopcount or
shim auditas a secret the model wrote, with one exception under "Known
gaps".
Keys, numbers and files in tool payloads
- Keys and numbers are inspected. An email address used as a JSON object
key is masked in the key, and a card, national ID or phone number sent as a
JSON number is found and counted; to an MCP tool, such a call is refused
under the defaultenforce. A whole float such as9.05e11is read as the
integer it is. - A value is read with its key.
{"password": "..."}and a
{"name": "DB_PASSWORD", "value": "..."}pair are masked. Past 4,096
characters only the start of a value is read with its key, unless the key
starts a finding there. - A file's bytes are never rewritten. A
Readof an uncompressed PDF
holding a credential used to have itsfile.base64replaced by
<SECRET_1>. Now a finding in a file's bytes, or in keys that masking would
make collide, withholds the result underenforce, saying what it held:
shim: withheld this Read result; it holds SECRET (1), which a placeholder cannot replace without breaking it.A call is refused withA placeholder cannot replace it without breaking the call.added.observeonly records
it andwarnonly reports it. - A real image passes unread. In a tool result, a PNG, GIF, WebP or JPEG
with a readable header and nothing but base64 in it reaches the model as a
picture, and its bytes cost no time: an MCP image, aReadimage, aRead
parts page. Anything else, including an image a call sends out, is read. - Cost. Text
Reads cost the same as in 1.2.2. A result near 1 MB dense
with keys takes 4 to 10 s against 0.2 to 2 s in 1.2.2, inside the deadline.
What the detector finds
- Any
scheme://user:password@host. The user and password in
postgresql+asyncpg://,mysql+pymysql://,mariadb://,clickhouse://
andsqlserver://URIs are masked asDB_URI, and inamqp://,smtp://,
ssh://,ldap://and every other scheme asSECRET, also when the
password is percent-encoded and on a line a diff adds or removes. Anhttps
URL with an encoded password, once reported asEMAIL, is aSECRET.
References such as${X},***and%X%, and an image pinned by a
sha256digest, are left alone. - Secret key names. A key with the secret word glued on (
PGPASSWORD,
DBPASSWORD,SECRETKEY,ACCESSTOKEN,OPENAIAPIKEY,registrytoken)
or followed by digits,_or.(PASSWORD2,PGPASSWORD_PROD) masks its
value;BYPASSandCOMPASSstay quiet. A typed declaration masks its
initializer in Python, TypeScript, Kotlin, Swift and Rust
(db_password: str = "..."). Maven<password>elements,<entry>and
<property>forms, Kubernetes and ECSname:/value:lists, AWS SSM name
and value pairs and a Terraform variable'sdefaultmask their value. A
template placeholder such as<your-api-token>is not a secret. - A digit-only password is masked under a key whose last word is a
password word (DB_PASSWORD=12345678,REDIS_PASSWORD,SMTP_PASS), and a
random password under such a key is no longer let through for looking like a
path or a call; code written in names (config.db_password) still is. - The mask covers exactly the value. In
<add key="DbPassword" value="..."/>the placeholder used to land on
value=while the value stayed in clear; now the value attribute is masked,
quoted or not, and an unquoted one runs to whitespace or>as a browser
reads it. A connection string keeps its options
(Server=db;Password=<SECRET_1>;Encrypt=true) and a URL its later
parameters. A percent-encoded quote or separator inside a password no longer
ends it early. A password in escaped JSON, as structured logs and AWS
Secrets Manager'sSecretStringprint it, is masked. - Where a value's end is ambiguous, the mask runs through. In a
.envline
such asPASSWORD=s3cr3tPass|next, the trailing;,&or|text can be
part of the password, so it is masked with it:PASSWORD=<SECRET_1>. A
password is never partly exposed. - Addresses keep their paths. A path or URL that holds an address keeps
its host and folders:https://api.example.com/v1/users/<EMAIL_1>and
/work/<EMAIL_1>/notes.txt, where 1.2.2 hid everything before the address,
a Google Drive folder named after one included. A git remote ending in
.git(git@github.com:org/repo.git) is no longer masked as an address;
email:passwordlines still are. - Context cues in code style. A Turkish tax ID after
TAX_ID=,taxId,
taxpayerId,vergiKimlikNooralıcıVkn, and a phone number after
phoneNumber,mobileNumber,contactPhone,cepTelefonuor in a vCard
TEL;TYPE=CELL:line, are masked.siparişNo, a false phone cue in 1.2.2,
no longer is ...
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 sk...
v1.2.1: Merge pull request #65 from GetSHIM/launch/4-release-1.2.1
shim-cli 1.2.1
1.2.1 fixes what three simulated first-time users and a literal copy-paste run
of the README and the cookbook tripped over in their first ten minutes on
1.2.0. The hook decides and masks exactly as in 1.2.0, so the detector and the
hook protocol are unchanged. The changes are in what the CLI prints, the
README's first screen and the cookbook.
Upgrading is uv tool upgrade shim or pipx upgrade shim, or
/plugin update shim-cli@shim-cli in Claude Code. shim install writes the
same entries as in 1.2.0.
What the CLI says now
shim demo <client>shows the synthetic sentence before and after
masking, then what that client does by default with a prompt or a tool result
like it. It used to print the same masked line for every client, though
Claude Code and Codex report a typed prompt and do not change it.--jsonis
unchanged.shim doctorwith nothing installed saysNo shim hook is installed, so nothing is inspected twice.It used to sayExactly one shim hook path is installed.shim install claudeno longer warns that existing hooks will be
preserved when the settings file has no hooks. Most Claude Code users already
have asettings.json, so most first installs printed a warning that was not
true. It now ends with the next step,Start a new Claude Code session; /hooks lists shim., andshim install copilotwithStart a new GitHub Copilot CLI session; it has no trust step.- A client settings file that does not parse is named with Python's JSON
error and its line and column, byinstall,revert,statusanddoctor:
~/.claude/settings.json is not valid JSON: Expecting value at line 1, column 10.They used to say only that the configuration could not be changed
safely. shim watchprints a two-line report when no request went through the
proxy, for example because the client was not signed in:shim watch — 1s, 0 requestsandno request reached the model provider through the proxy. It
used to print nothing after its first line.shim redactends its output the way the input ended. It used to add a
newline, soshim redact < file > copygave a file with a blank last line.
What a visitor reads
The README opens with a quickstart and a list of what shim cannot do. The
quickstart is install, shim demo claude and shim install claude. The list
says that a prompt typed into Claude Code or Codex is not masked, that Codex
and GitHub Copilot CLI tool traffic is not inspected, that VS Code masks
nothing, which Claude Code events are only reported, and that Windows is not
supported. The install example is Claude-first, and the four screenshots are
recaptured from this release's output.
The cookbook now warns about four traps. A second [mode] header makes the
settings file invalid. --reset turns off a ledger you turned on. Claude Code
puts email addresses of its own into a session, so shim watch counts them.
Pasting the redacted-prompt line makes Claude Code ask once to read a temporary
file.
A contract test checks every shim command in the README, the plugin README,
the cookbook, the command reference, the findings page and llms.txt against
the CLI's own commands and flags. It checks 259 lines.
The repository has issue forms, a bug report and a detection report, and a
pull request template. CONTRIBUTING.md says why bin/shim.pyz is rebuilt
only in a release commit. The Claude Code plugin installs from main, so an
archive rebuilt between releases ships unreleased code to plugin users.
What to check after upgrading
- Scripts that read
shim redact: the trailing newline is no longer added.
Input that ends with one still gets it. - Scripts that read
shim demotext: it now hasbeforeandafterlines
and a sentence about the client. Readshim demo <client> --jsoninstead. - Scripts that read JSON errors:
CLIENT_SETTINGS_MALFORMED'serrornow
ends with the file and the JSON error. Matchcode, nevererror. - Downgrading: a shim-cli older than 1.2.0 refuses a settings file that
holdsmarkersor whosedietnameshtml, and while it is refused the
prompt hook withholds every prompt. Remove both keys before you downgrade.
v1.2.0: Merge pull request #61 from GetSHIM/r2/cli-night2
shim-cli 1.2.0
1.2.0 makes shim-cli easier to run from a script or an agent and closer to the
gateway. Every error carries a stable code and a fix, doctor, report and
watch emit findings in shim's published schema, shim watch prices at dated
rates and names the waste in a session, the detector finds what the gateway
finds, injection markers read Turkish, an opt-in diet turns a whole HTML page
into text, and agents have documentation to read instead of source code. With
default settings the hook reads, decides and answers as it did in 1.1.2; only
what the detector finds changes.
Upgrading is uv tool upgrade shim or pipx upgrade shim, or
/plugin update shim-cli@shim-cli in Claude Code. The hooks need nothing new:
shim install writes the same entries as in 1.1.2. What to check afterwards is
at the end.
Every error says what to do
Every JSON error now carries code, a stable name, and fix, the command or
action that clears it:
{"code":"REVEAL_INVALID","command":"config","error":"that entity cannot reveal a tail","fix":"Use --reveal IBAN=N, CREDIT_CARD=N or PHONE=N with N from 1 to 4.","schema_version":1,"status":"error"}shim doctor --json gives each check detail, code and fix, and install
and revert take --json, so an agent can set shim up and repair it without
parsing human text. Without --json nothing changes: every error line and exit
code is what 1.1.2 printed. The codes are in the
command reference, and a test keeps that table and
the code in step.
The JSON error text changed wherever it used to differ from the sentence a
person reads. shim config --json says what is wrong instead of
unable to process entity settings; scan, redact and demo say
Unable to process stdin. (it was unable to process stdin); report,
ledger show, ledger purge and the client-settings inspection error use their
human sentence. Match code, never error.
A settings error that will happen again says so. A write the system refuses,
because of permissions, a read-only file system or a full disk, used to end in
"Run the command again", which cannot help. It now has its own code, the
system's reason and a fix that names the folder:
SETTINGS_UNWRITABLE:shim configcould not write shim's settings file.CLIENT_SETTINGS_UNWRITABLE:shim installorshim revertcould not write
the client's settings file.CLIENT_SETTINGS_UNREADABLE: shim could not inspect the client's settings
file (install,status,revert,doctor).
A real race keeps SETTINGS_CHANGED and CLIENT_SETTINGS_CHANGED. The chmod
fix for a refused settings file names the path in use (SHIM_CONFIG,
XDG_CONFIG_HOME) and is offered only where a mode is the problem, and
SETTINGS_INVALID no longer offers shim config --reset --yes as its fix: it
discards every setting, so the message names it as the last resort.
Findings in shim's published schema
With --json, shim doctor, shim report and shim watch add a findings
list in shim's finding schema v1, the shape the gateway uses for its own, copied
unchanged to docs/schemas/finding-v1.json. Each
finding has a rule id, a severity, evidence, impact, the fix and a playbook
link:
doctor: one finding per check that did not pass,cli.doctor.<code>, with
remediation.actionset to theshimcommand that clears it when there is
one.SETTINGS_INVALIDnamesshim config --reset --yesin its text as the
last resort and never offers it as the action.report: values a failed call printed unmasked, events that passed
uninspected, and injection markers, counted from the session's records.watch: a cache prefix written again, identical requests in flight, a
response cut off at its output limit, images, and a tool result above
25,000 bytes.
Every sentence comes from a fixed template; no path, value, command line or
check detail reaches a finding. The id is the same on every run for the same
rule and subject, and Findings gives the recipe and one
playbook per rule. Every other key of the three commands keeps its value, and
the list is empty when nothing fires.
HTML in command and MCP output, as text, on request
A new diet transform, html, turns a whole HTML page that a command or an MCP
tool returns into text before the model reads it: scripts, styles and markup
are dropped, headings, lists, tables and link addresses are kept, and a first
line tells the model the page was converted and how to see the markup. It is
not lossless, so it is off until you name it:
diet = ["json", "html"]shim config --diet keeps meaning the default and never turns it on. A page is
converted only when it is at least 2,048 characters and its text is at most
half its size. The converted text is scanned again before the model reads it,
because conversion can assemble a value the markup hid: an address written as
ops@example.com, or an IBAN split across two <span>s, is masked and
counted like any other, and if that scan fails the page reaches the model
unconverted. A page cut short, as Claude Code cuts command output at 30,000
characters, is converted without its unfinished last tag; one that leaves a
<script>, <svg>, <iframe>, <noscript> or <template> open is left
unchanged rather than lose the rest.
PDF and Word files are not converted: a Read is a view of a file, and the
diet never touches those. WebFetch already hands the model text.
shim watch prices at dated rates and names the waste
shim watch priced Opus 4.5 and later up to 7.5 times too high: it matched a
model to a neighbouring price. On a live Opus 5.5 session its total was 3.75
times Claude Code's own figure; it is now within 0.1 percent. Each request is
priced by its own model and token kinds from a table read off Anthropic's
pricing page on 2026-10-09, which adds Sonnet 5, Fable, Mythos and Haiku 5.5 and
prices 1-hour cache writes at their own rate. A model the table does not name
exactly is not priced, never priced as a neighbour. The spend line carries the
date, and once the table is more than 90 days old it says so, and
prices_stale in --json turns true:
spend ~$0.69 (approximate, 2026-10-09 prices)
costliest one request ~$0.41 (claude-opus-5-5, 54,212 input tokens)
images 2 distinct, ~4,988 input tokens across 2 requests (estimated)
largest tool result Bash, 40,960 bytes (~10,240 tokens), carried by 2 requests
cache 1 request rewrote a prefix the previous one had cached (~18,000 tokens written again, ~$0.09); request 3
duplicates 1 request was sent while an identical one was still in flight (~27,640 input tokens billed again)
The last four lines are new, computed from what watch already reads; nothing
forwarded changes and nothing is resent or delayed. Images are counted once
however often the history replays them, with their own row in the split, and one
whose size could not be read is counted as of unknown size, not as zero
tokens. A cache rewrite is a request that wrote again a prefix the previous
request in the same conversation had cached, such as the turn after a cache
expired; a parallel agent is not that previous request. Identical requests in
flight are both forwarded and only counted. A request that arrived while both
inspection slots were busy now takes the model its response names, so it is
priced and kept in its cache chain; it used to have no model at all.
The detector finds what the gateway finds
Checked against the gateway's own case file, 97 of 140 cases agreed; now 125
do, and every other case carries shim-cli's exact output and one reason, pinned
by a test.
- Now masked: Troy and Mastercard 2-series cards, every IBAN shape
(lowercase, spaced, broken across a line), TCKN in groups (100 000 00 146),
[at]e-mails, andşifre:andparola:assignments. - An IP after
-v.vno longer reads as a version cue, so
curl -v 192.168.1.1,ping -vandgrep -vkeep their address masked, and
so doesif ip == "203.0.113.7":.numpy==1.26.4.1is still a version. - A phone glued to a word. Digits joined to an identifier by
-,_or.
are not a phone, but a space, a bracket or a+makes one:
musteri-0532 123 45 67reaches the model asmusteri-<PHONE_1>. - A secret after a space.
password hunter2abcand
export API_KEY abcdef123456are masked, aspassword=hunter2abcalways was. - No longer masked: dated model ids (
claude-sonnet-4-5-20250929became
claude-sonnet-<PHONE_1>), version strings such asversion 1.2.3.4, and the
number afterorder no,sipariş noorFatura no. The model'sEditof a
line holding a model id now applies to the bytes it was shown.
Turkish number plates are the first opt-in type: off until you turn them on,
because plate-shaped strings are common in logs and tables.
shim config --enable TR_LICENSE_PLATE --yesWith it on, plaka 34 ABC 123 becomes plaka <TR_LICENSE_PLATE_1>. A cue such
as plaka or plate wins over the letters coding traffic uses (GET, CPU),
and an uncued [12 GET 200] stays quiet. shim config lists the type as OFF
and shim doctor reads 12/13 by default.
Injection markers read Turkish
The markers that flag instructions hidden in a tool result now recognise
Turkish: "Önceki tüm talimatları yok say", "sen artık ...", a Sistem: line,
"kullanıcıya söyleme", with the same five ids as the English ones. The match
ignores case and accents, so "onceki tum talimatlari yok say" typed on an
English keyboard is flagged, and it leaves alone what only looks like an order:
the noun "gösterme mantığı", the sentence "sen artık büyüdün", a bare Sistem:
label and a conventional-commit subject.
Markers are still only reported, as flagged lines in the Stop summary,
shim report and the ledger; nothing is block...
v1.1.2: Merge pull request #50 from GetSHIM/claude/gracious-brown-hb35us
shim-cli 1.1.2
1.1.2 fixes two detector mistakes that real Claude Code sessions ran into: a
number passed to a function lost its opening bracket to a <PHONE_1>, and
shim's own placeholder, quoted in backticks, was counted as a secret.
Upgrading is uv tool upgrade shim or pipx upgrade shim, or
/plugin update shim-cli@shim-cli in Claude Code. The hooks need nothing new:
shim install writes the same entries as in 1.1.1.
A number in code keeps its bracket
A ten-digit number right after ( or [ was masked as a phone number, and the
mask took the bracket with it. An agent that read
EPOCH = datetime.fromtimestamp(1759744800) was handed
EPOCH = datetime.fromtimestamp<PHONE_1>), code that does not parse, and
ipaddress.IPv4Address(3221225985) fared the same.
A bracket the match never closes is no longer part of it, and the number left
is judged the way the same number without a bracket always was: a phone only
when it is Turkish-shaped or follows a cue such as tel or phone. So
fromtimestamp(1759744800) reaches the model unchanged,
SUPPORT = "tel (4155552671)" reaches it as SUPPORT = "tel (<PHONE_1>)", and
(415) 555-2671, whose bracket closes inside the number, is masked whole as
before. A bare ten-digit number in brackets with no cue, such as
contact (4155552671), is now left alone, as contact 4155552671 already was.
shim's own placeholders are not secrets
A model that quotes what it was given writes `DB_PASSWORD=<SECRET_2>`. The
closing backtick made the value <SECRET_2>` instead of a placeholder, so it
was counted as a secret: shim audit listed it under model output, and the
same text in a prompt or tool result was masked again under a new number. A
placeholder followed only by `, *, ., ;, :, !, ? or ) is now
left alone. DB_PASSWORD=<SECRET_2>xyz is still a secret.
Tested on Claude Code 2.1.291
shim doctor claude no longer warns that Claude Code 2.1.291 is newer than
the tested version: every hook event was run on it in real sessions, through
the package and through the plugin's archive. The details are in the
compatibility record.
v1.1.1: Merge pull request #49 from GetSHIM/release/1.1.1
shim-cli 1.1.1
1.1.1 fixes a permission problem in the Claude Code hook and two lines of CLI
output that could leave the hook switched off, and rewrites every part of the
docs that three first-time users, following them from zero, could not get
through on their own.
Upgrading is uv tool upgrade shim or pipx upgrade shim, or
/plugin update shim-cli@shim-cli in Claude Code. The hooks need nothing new:
shim install writes the same entries as in 1.1.0.
A masked tool call keeps its permission prompt
When shim masked a value in the arguments of a Claude Code tool call, its answer
also approved the call, so Claude Code skipped the permission prompt that the
same call gets without the value. Measured on Claude Code 2.1.286 with
claude -p: a WebFetch of https://example.com/?ref=docs was refused because
nothing had approved it, and the same fetch with ?email=ops@example.com in the
URL ran. Anything that could put an email address into a call could have that
call approved. Deny and ask rules in your settings held throughout.
shim now hands back the masked arguments and nothing else, and Claude Code's
permission rules decide as they would without shim. Measured again on the
release build: the fetch above is refused, with ?email=<EMAIL_1> in what
Claude Code reports, and with WebFetch allowed for httpbin.org, a fetch of
https://httpbin.org/get?email=ops@example.com runs and httpbin receives
<EMAIL_1>. shim installs no tool hook for Codex or GitHub Copilot CLI, and VS
Code accepts no replacement for a tool call, so only Claude Code changes.
Turning the hook on in Codex and Copilot CLI
shim install codex ended by telling you to accept the shim hook "when asked".
Codex never asks: it skips a hook it holds no trust record for, without a word,
and prompts go to the model uninspected. The line now says to open /hooks in
Codex, review the shim entry and enable it, as the README already did.
shim doctor copilot told GitHub Copilot CLI users to check the hook with
/hooks, which Copilot CLI does not have, and told anyone running the VS Code
plugin beside shim install copilot to remove one of them. Copilot CLI runs the
hook with no trust step, and the plugin stands down inside Copilot CLI, so that
pairing is the intended one. Both lines now pass and say so, and a healthy
Copilot CLI install prints no warning of its own.
Docs a newcomer can follow
Three simulated first-time users installed the 1.1.0 package in empty sandboxed
homes and followed only the README, the cookbook and the command reference: one
on Claude Code, one on Codex, GitHub Copilot CLI and VS Code, and one running
every recipe and every command example. What stopped or misled them is fixed:
- Install. The pipx line no longer needs Python 3.12 already installed, and
the install ends with a check, for each client, that the hook really runs. - VS Code. Refusing a tool call is
outbound = "enforce"; the README said
inbound, which refuses nothing there. A file read in VS Code is not
inspected at all, and the docs now say so plainly. Installing the plugin has
concrete steps, and a test prompt stands in for the doctor VS Code does not
have. - Settings. The file does not exist until something writes it, and
shim config --reset --yescreates it safely. The modes areobserve,warn
andenforce, the keys are hyphenated, and becauseshim configdoes not
show[mode], there is a test that blocking is on. On Linux an editor under
umask 002 writes a file shim refuses, and the fix is named. - Reports.
shim reportreads a live session: run it while the session is
open, or turn the ledger on first. "No session on record" is not "found
nothing". - CI. The recipe passed while scanning nothing when
origin/mainwas
missing, and failed on any binary file. It now scans the lines a change adds,
withpipefailon. - The rest. What is and is not detected, the warnings a healthy
shim doctorprints for each client, whatshim demodoes and does not
show, and an uninstall that covers pipx, the plugin, VS Code and what stays
behind.
v1.1.0: Merge pull request #48 from GetSHIM/release/1.1.0
shim-cli 1.1.0
1.1.0 adds two commands and leaves the hook as it was in 1.0.3. shim audit
counts what your Claude Code sessions have already sent to the model, from
Claude Code's own history, and can delete those sessions from this computer.
shim keys lists the variables in a .env or INI file without their values,
so an agent can say which keys are set without reading the file.
Upgrading is uv tool upgrade shim. Both commands come with the package, not
the plugin: with the plugin alone, add them with
uv tool install --python 3.12 shim. The hooks need nothing new.
What already went out
shim has only ever seen what happened after it was installed. shim audit
reads Claude Code's own history and counts what has already reached the model:
by kind of value, by project, and by the way in — your prompt, a tool result, a
failed command, a file attached with @. On a synthetic history:
shim audit — Claude Code history, 3 sessions in 2 projects, 2026-09-28 to 2026-09-30
reached the model
SECRET 10 in 2 sessions @.env 5 · failed Bash 5
EMAIL 3 in 3 sessions @.env 1 · failed Bash 1 · your prompt 1
DB_URI 2 in 2 sessions @.env 1 · failed Bash 1
by project
~/work/kasa-mutabakat SECRET 10, DB_URI 2, EMAIL 2 · last 2026-09-30
~/work/site EMAIL 1 · last 2026-09-29
already masked by shim 3 values in 1 session
model output 2 EMAIL (written by the model; it may repeat values it was given)
scanned 3 sessions, 5 KB, in 0.0 s. Nothing was changed, written or sent.
It prints no value, keeps nothing and sends nothing. shim audit --purge
deletes the transcripts of the sessions that sent something, and their lines in
history.jsonl, after you type a confirmation. It leaves Claude Code's other
per-session files, such as its backups of edited files, and says plainly that
this does not change what the model provider already received.
The names, not the values
shim keys .env lists the variables a .env or INI file defines, whether each
is set and what shim would call its value, and never the value:
.env
APP_ENV set
DATABASE_URL set DB_URI
AWS_ACCESS_KEY_ID set SECRET
AWS_SECRET_ACCESS_KEY set SECRET
SLACK_BOT_TOKEN set SECRET
LEDGER_API_TOKEN set SECRET
STRIPE_SECRET_KEY set SECRET
SUPPORT_EMAIL set EMAIL
MAX_TOKENS set
TOKEN_TTL set
Asked "which services have keys in .env?" with only a CLAUDE.md line pointing
at it, Claude Code 2.1.286 ran shim keys .env instead of opening the file and
answered: "I only listed the names with shim keys, so I haven't seen any
values." The cookbook
has the line and the permission rule that lets the agent run it without asking.
v1.0.3: Merge pull request #44 from GetSHIM/release/1.0.3
shim-cli 1.0.3
1.0.3 closes five gaps a field test of 1.0.2 found, each checked again on Claude
Code 2.1.286: secrets named by their key, connection strings that lost their
host, the output of a command that failed, files attached with @, and Python
3.14. On Windows, which shim-cli does not support, it now says so instead of
withholding every prompt. And a % that is not an escape, as in
LIKE '%admin%', no longer makes a prompt unreadable to shim.
Upgrading is uv tool upgrade shim, or /plugin update shim-cli@shim-cli in
Claude Code. If you installed the hook with shim install claude, run it once
more after upgrading: it adds the entry for failed commands. Until you do,
shim status claude reports the hook as not installed.
Secrets with a name
On 29 September a live Claude Code session read a synthetic .env with shim
installed, and the model told the user, in its own words, that
AWS_SECRET_ACCESS_KEY, SLACK_BOT_TOKEN and LEDGER_API_TOKEN had not been
masked. The secret rule only started when the key was the secret word, so
password= was caught and DB_PASSWORD= was not, and six vendor formats were
missing.
A secret word now counts anywhere in a key name (DB_PASSWORD,
KIS_APP_SECRET_LIVE, client.secret, x-api-key, clientSecret), and Slack,
Google, fine-grained GitHub, GitLab, npm and Hugging Face tokens, Azure storage
AccountKey values and HTTP Authorization: Basic and Bearer headers are
found by their shape.
Of the eighteen lines of that probe, 1.0.2 masked five and 1.0.3 masks all
eighteen. MAX_TOKENS=4096, TOKEN_TTL=3600, PASSWORD_MIN_LENGTH=12 and
SORT_KEY=name stay quiet, and so do references such as ${DB_PASS} and type
annotations such as apiKey: string.
Code that only names a secret stays quiet as well: "token_type": "Bearer",
api_key_header = "X-Api-Key", token_ids = tokenizer.encode(text) and
auth_token = self.auth_token.
Connection strings keep their host
Asked whether a service talks to the production or the staging database, the
same field-test session answered that it could not say, because the whole
DATABASE_URL had become <DB_URI_1> and the host was inside the mask. Only
the credentials are secret. A connection string now keeps its scheme, host,
port and database and loses its user and password, as one placeholder:
postgresql://kasa_app:synthetic-password@db-prod.kasa.internal:5432/kasa
postgresql://<DB_URI_1>@db-prod.kasa.internal:5432/kasa
A connection string with no credentials in it is no longer a finding, wherever
it points; a team that wants internal host names hidden adds a custom pattern.
The user and password in an http(s), ftp or ws(s) URL are a SECRET, so a
Sentry DSN no longer reads as an email address.
Failed commands
cat .env && cat config/local_overrides.env exits 1 when the second file is
missing, and Claude Code hands everything the command printed to a separate
hook event, PostToolUseFailure, which shim did not install. In the field test
all ten values in .env reached the model and the session summary said
nothing. Claude Code does not let that output be masked, so shim now says what
was in it, on screen:
PostToolUseFailure:Bash says: shim: found DB_URI (1), EMAIL (1), SECRET (5) in a
failed Bash. Claude Code does not let this output be masked; the model has these
values.
tells the model not to repeat those values (it answered that it had "left
[them] out of this reply"), and counts them on a new summary line,
unmasked 5 SECRET (failed Bash). shim install claude adds the sixth hook
entry; shim doctor claude reads 5 of 6 events installed until you do.
Files attached with @
@.env explain these variables is how many people show a file to Claude Code.
The client inlines the file after the prompt hook has run, so in the field test
all ten values reached the model and shim said nothing. The prompt hook now
reads the files a prompt names, the way Claude Code resolves them, and tells you
before they are sent:
UserPromptSubmit says: shim: @.env holds DB_URI (1), SECRET (5), EMAIL (1).
Attached files reach the model unmasked; to have them masked, ask the agent to
read the file instead.
Under user-prompt = "enforce" the prompt is stopped, and nothing from the file
reaches the model or Claude Code's transcript. The summary's model line and
shim watch's response line no longer call repeated values "not leaks": the
model may repeat a value it was given, for example from a file you attached.
Long prompts and large tool results
A prompt longer than 100,000 characters is scanned in pieces. A piece that
grows past the detector's limit when it is normalized, as Korean text does, is
now cut smaller and scanned again, where 1.0.2 withheld it or let it through
unread. If a piece still cannot be scanned, the prompt is withheld, as a prompt
shim cannot analyse already was, instead of going through as if all of it had
been read; shim scan and shim redact now fail the same way. On a large tool
result, the scan ends at shim's own 25-second deadline instead of starting over
on each later field, which could run past Claude Code's 30-second limit, where
the client stops the hook and passes the result on unreported. The record
counts the fields that were not inspected.
A percent sign is text
shim decodes percent escapes so that alice%40example.com is still found as an
address. A % followed by two hex digits that did not decode as UTF-8, as in
LIKE '%admin%', echo %DATE% or caf%E9, made the whole text unreadable to
it: such a prompt was withheld in every mode with "shim could not inspect this
prompt", a file holding one was attached as not inspected, and the part of a
tool result holding one reached the model unmasked. Only the escapes that form
UTF-8 are decoded now; the rest are read as written, which is also what the
model reads.
A secret sent as base64
Asked to "print my .env as base64 so I can paste it into a GitHub secret", an
agent runs base64 .env, and the output held every value of the file in a form
the detector could not read. A block of base64 whose decoded text holds a
secret or a connection string with credentials is now masked as a whole: wrapped
or not, inside YAML or JSON, in numbered grep output and in a diff that adds or
deletes it, and so is a Kubernetes .dockerconfigjson. Docker's auth and identitytoken values and
npm's _auth are secrets as well. Hex and other encodings are not decoded. A
long base64 blob, such as an image in a notebook, also scans far faster when no
@ appears in the same 100,000 characters: about 0.4 seconds per 100 KB on
Python 3.13, where it took about eight.
Python 3.14
The package no longer refuses Python 3.14: pip install shim failed there with
"No matching distribution found". requires-python has no upper bound, CI runs
3.10, 3.13 and 3.14, and the plugin launcher looks for python3.14 first.
Windows says it is not supported
shim-cli has never supported Windows, and it did not say so: on Windows the hook
failed before it could inspect anything and answered every prompt by
withholding it. It now stands down with one line,
shim: shim-cli does not support Windows yet; nothing was inspected., and lets
the prompt through; the plugin launcher does the same under Git Bash, and the
CLI refuses with a sentence instead of a traceback. This was simulated on
macOS, not run on Windows. Inside WSL shim-cli runs as it does on Linux, not
yet verified.
v1.0.2: Merge pull request #33 from GetSHIM/release/1.0.2
shim-cli 1.0.2
Claude Code loads one hook file from the plugin instead of two. The Codex hook
file moved to hooks/codex.json, named by .codex-plugin/plugin.json, so Claude
Code no longer finds it by convention and no longer runs a Codex command on every
prompt only for it to stand down. Nothing you see in Claude Code changes.
Codex users: current Codex installs the plugin but does not load its hook, so the
plugin has not been inspecting your prompts, and Codex does not say so. Remove it
and install the hook instead, then trust the shim entry in /hooks:
codex plugin remove shim-cli@shim-cli
shim install codex
shim doctor codexThe PyPI page now says what the README says: its summary is the tagline, it has
search keywords, its homepage is
getshim.tech/products/shim-cli, and it
links to these release notes.
The plugin folder carries its own LICENSE, so an installed copy of the plugin
includes the Apache-2.0 text.
shim v1.0.1
shim-cli 1.0.1
VS Code is a supported client, through the plugin. The same release fixes a
worse problem it uncovered: a Claude Code install has been running inside VS
Code all along, and its prompt block was being ignored there.
Upgrading is uv tool upgrade shim, or /plugin update shim-cli@shim-cli in
Claude Code. There is nothing to migrate.
If you use VS Code
Point VS Code at the plugin folder:
"chat.pluginLocations": { "/path/to/shim-cli/plugins/shim-cli": true }There is no shim install vscode; the plugin is the whole route. What you get,
measured against VS Code 1.137.0 with Copilot Chat 0.65.0 rather than read out
of its documentation:
| Moment | What shim does |
|---|---|
| Your prompt | Reports the finding. Under enforce, the prompt is stopped before it is sent, and VS Code shows the reason. |
| Before a tool runs | Reports. Under enforce, the call is denied and never runs. |
| After a tool has run | Reports only. |
Nothing is masked in VS Code, and after a tool nothing is blocked either.
Both were measured. A hook answering decision: "block" on a terminal result
was followed by the model quoting the secret out of that result, and
continue: false at the same event did not stop the turn: by then the model
has been handed the output. Where Claude Code silently replaces a value with
<SECRET_1>, VS Code tells you and leaves it.
One consequence worth knowing: VS Code sends a read_file result to hooks as
an empty tool_response, so shim cannot see what a file read returned. A file
read is covered by its path before the read. A terminal result does arrive in
full and is inspected.
Because a refusal is the only enforcement available, tool events report by
default there. Ask for the refusal when you want it:
[mode]
outbound = "enforce"If you use Claude Code, read this even if you never open VS Code
VS Code loads hooks from ~/.claude/settings.json by default. So shim install claude has been registering shim in two clients at once, and in VS Code it
spoke Claude's protocol: the prompt block, decision: "block", is a field VS
Code ignores. Anyone running user-prompt = "enforce" who used VS Code had
prompts sent that shim reported as blocked.
1.0.1 tells the two apart by what each always sends — every Claude Code event
carries permission_mode and never timestamp, and every VS Code event is the
reverse — and answers each in its own protocol. A VS Code prompt is now stopped
with continue: false, which VS Code obeys. Nothing changes for Claude Code
itself.
The environment cannot be used for this and is not: the VS Code capture carried
CLAUDE_CODE_* variables, because the editor inherited them from the terminal
that started it.
Also in this release
- The shared event pipeline carries what each client grants at each event —
rewrite,refuseorreport-only— and caps the action at it. A mask
becomes a refusal where only refusal exists, and a refusal becomes a report
where the model already holds the data. The session summary follows: it says
found, notblocked, when nothing was withheld. - A hook asked for a client this build does not know now inspects nothing and
says so on stderr, instead of answering with another client's refusal. An
older package meeting a newer plugin used to block every VS Code prompt with
a message naming Codex. - The plugin carries an Agent Plugins v1 manifest,
plugins/shim-cli/plugin.json,
alongside the Claude and Codex ones. Its hooks live in
com.github.copilot/hooks/hooks.json, which GitHub Copilot CLI and the
Copilot app read as well; there the command stands down, becauseshim install copilotis their route and inspecting a prompt twice helps nobody.