Skip to content

Releases: GetSHIM/shim-cli

v1.2.3

Choose a tag to compare

@github-actions github-actions released this 20 Oct 08:23
a46f394

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, MultiEdit or
    NotebookEdit, a VS Code edit tool (for apply_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 as EMAIL 1 3, never a value. A masked tool result,
    the redacted copy of a prompt blocked under enforce and 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, and SessionEnd deletes 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 says deny with
    the note placeholder.

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 a Read after it are right.
  • The html diet 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 Stop count or
    shim audit as 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 default enforce. A whole float such as 9.05e11 is 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 Read of an uncompressed PDF
    holding a credential used to have its file.base64 replaced by
    <SECRET_1>. Now a finding in a file's bytes, or in keys that masking would
    make collide, withholds the result under enforce, 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 with A placeholder cannot replace it without breaking the call. added. observe only records
    it and warn only 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, a Read image, a Read
    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://
    and sqlserver:// URIs are masked as DB_URI, and in amqp://, smtp://,
    ssh://, ldap:// and every other scheme as SECRET, also when the
    password is percent-encoded and on a line a diff adds or removes. An https
    URL with an encoded password, once reported as EMAIL, is a SECRET.
    References such as ${X}, *** and %X%, and an image pinned by a
    sha256 digest, 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; BYPASS and COMPASS stay 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 ECS name:/value: lists, AWS SSM name
    and value pairs and a Terraform variable's default mask 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's SecretString print it, is masked.
  • Where a value's end is ambiguous, the mask runs through. In a .env line
    such as PASSWORD=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:password lines still are.
  • Context cues in code style. A Turkish tax ID after TAX_ID=, taxId,
    taxpayerId, vergiKimlikNo or alıcıVkn, and a phone number after
    phoneNumber, mobileNumber, contactPhone, cepTelefonu or in a vCard
    TEL;TYPE=CELL: line, are masked. siparişNo, a false phone cue in 1.2.2,
    no longer is ...
Read more

v1.2.2

Choose a tag to compare

@github-actions github-actions released this 18 Oct 09:18
9a18663

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: ToolSearch keeps the tool names it found, which are
    Claude Code's own and not content, and a Read of an image or a PDF is
    withheld as a text Read. You see shim: 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 unreadable or analysis 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_dir and the like) is never rewritten or refused. A
    finding there shows shim: found EMAIL (1) in Read. Not modified. under the
    new local-tool direction, whose default is warn; local-tool = "observe"
    silences it. The answer you type to AskUserQuestion is local-tool too,
    so it is no longer masked as a tool result.
  • Web tools are masked, and you are told. WebFetch and WebSearch
    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
    default enforce, 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. Monitor and mcp__ide__executeCode are
    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_task and run_playwright_code are executable-text, and
    the file-editing tools are local-write, so those two keys now apply there.
  • The diet reshapes only web and MCP tool results. Command output, curl
    through Bash included, 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.json and config.toml,
    ~/.copilot/hooks/shim.json), or to any file in the plugin folder running
    the hook;
  • an agent's shell command that runs shim with config, revert or
    uninstall, every shim config included, 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 as shim: 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...
Read more

v1.2.1: Merge pull request #65 from GetSHIM/launch/4-release-1.2.1

Choose a tag to compare

@github-actions github-actions released this 11 Oct 22:43
c0bc6b1

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. --json is
    unchanged.
  • shim doctor with nothing installed says No shim hook is installed, so nothing is inspected twice. It used to say Exactly one shim hook path is installed.
  • shim install claude no longer warns that existing hooks will be
    preserved when the settings file has no hooks. Most Claude Code users already
    have a settings.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., and shim install copilot with Start 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, by install, revert, status and doctor:
    ~/.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 watch prints a two-line report when no request went through the
    proxy, for example because the client was not signed in: shim watch — 1s, 0 requests and no request reached the model provider through the proxy. It
    used to print nothing after its first line.
  • shim redact ends its output the way the input ended. It used to add a
    newline, so shim redact < file > copy gave 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 demo text: it now has before and after lines
    and a sentence about the client. Read shim demo <client> --json instead.
  • Scripts that read JSON errors: CLIENT_SETTINGS_MALFORMED's error now
    ends with the file and the JSON error. Match code, never error.
  • Downgrading: a shim-cli older than 1.2.0 refuses a settings file that
    holds markers or whose diet names html, 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

Choose a tag to compare

@github-actions github-actions released this 11 Oct 18:29
62a32fb

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 config could not write shim's settings file.
  • CLIENT_SETTINGS_UNWRITABLE: shim install or shim revert could 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.action set to the shim command that clears it when there is
    one. SETTINGS_INVALID names shim config --reset --yes in 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&#64;example&#46;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: and parola: assignments.
  • An IP after -v. v no longer reads as a version cue, so
    curl -v 192.168.1.1, ping -v and grep -v keep their address masked, and
    so does if ip == "203.0.113.7":. numpy==1.26.4.1 is 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 67 reaches the model as musteri-<PHONE_1>.
  • A secret after a space. password hunter2abc and
    export API_KEY abcdef123456 are masked, as password=hunter2abc always was.
  • No longer masked: dated model ids (claude-sonnet-4-5-20250929 became
    claude-sonnet-<PHONE_1>), version strings such as version 1.2.3.4, and the
    number after order no, sipariş no or Fatura no. The model's Edit of 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 --yes

With 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...

Read more

v1.1.2: Merge pull request #50 from GetSHIM/claude/gracious-brown-hb35us

Choose a tag to compare

@github-actions github-actions released this 06 Oct 11:50
ac64452

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

Choose a tag to compare

@github-actions github-actions released this 04 Oct 09:26
221240f

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 --yes creates it safely. The modes are observe, warn
    and enforce, the keys are hyphenated, and because shim config does 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 report reads 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/main was
    missing, and failed on any binary file. It now scans the lines a change adds,
    with pipefail on.
  • The rest. What is and is not detected, the warnings a healthy
    shim doctor prints for each client, what shim demo does 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

Choose a tag to compare

@github-actions github-actions released this 03 Oct 09:52
d1cabc6

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

Choose a tag to compare

@github-actions github-actions released this 03 Oct 09:52
761d548

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

Choose a tag to compare

@github-actions github-actions released this 29 Sep 10:29
962be0f

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 codex

The 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

Choose a tag to compare

@github-actions github-actions released this 20 Sep 10:30
4b68ff1

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, refuse or report-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, not blocked, 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, because shim install copilot is their route and inspecting a prompt twice helps nobody.