Repository navigation
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 blocked or rewritten. One new setting
tells the model as well. With markers = "note" in the settings file, a Claude
Code PostToolUse result that carries a marker gets one sentence beside it,
saying that it reads as instructions and is data from the tool. The result is
unchanged. On a synthetic runbook.md that told the model to ignore its earlier
instructions, Claude Code 2.1.286 said the file held such a line and that it did
not act on it.
Documentation for agents
The repository ships an llms.txt and a context7.json that send an agent, or
Context7, to the README, command reference, cookbook, privacy and compatibility
pages instead of the source code, with eight rules an agent should never get
wrong. Asked how to make shim block a prompt holding a secret, Claude Code
2.1.286 in an empty folder knew of no such tool; given only llms.txt and the
pages it links, it named user-prompt = "enforce" under [mode]. The cookbook
has a recipe, Let an agent run shim, and
the command reference now lists shim scan --json's fields.
What to check after upgrading
- Scripts that read JSON errors: read
code.errorchanged for several
commands and may change again. doctor,reportandwatchwith--jsongainfindings; a script
that rejects unknown keys must allow it.shim watchtotals change for every session on Opus 4.5 or later,
Sonnet 5 or later, Fable, Mythos or Haiku 5.5, and with 1-hour cache writes,
because the old figures were wrong. In a session with images, the split and
tokens_by_sectionmove tokens frommessagesto animagesrow; the total
is unchanged.--jsongainsimages,largest_tool_result,
cache_rewrites,duplicates_in_flight,spend_usd,priced_asand
cache_creation_1h_input_tokens; existing keys keep their meaning.- Detection changes in both directions, as listed above. A settings file with
an explicitenabled_entitieslist is unaffected until you add
TR_LICENSE_PLATE.shim watchuses the default set, so it never counts
plates. - Downgrading: an older shim-cli refuses a settings file that holds
markers, as it does any unknown key, and one whosedietnameshtml.
Remove both first.