Skip to content

v1.2.2

Choose a tag to compare

@github-actions github-actions released this 18 Oct 09:18
· 29 commits to main since this release
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 skipped. A broken shim-hook on
    PATH (a virtual environment that lost shim, an interpreter that is gone, a
    first line that names no Python) used to switch the bundled archive off. The
    launcher now falls through to the archive, and through archive candidates
    that cannot start, such as a pyenv shim whose version is missing or the macOS
    xcode-select stub.
  • Python runs isolated. The archive runs with -I and the package script
    with -E, so PYTHONPATH=. and a project file named token.py or
    types.py can no longer run inside the hook and switch masking off.

Installing, the CLI and shim watch

  • shim install checks the hook it writes. Before writing, and on
    --dry-run, it runs the exact command it registers on a synthetic safe and
    a synthetic sensitive prompt. A shim installed with pip install --user used
    to install and pass shim status while its -I hook could not import shim
    and let every event through. Now install refuses with DETECTOR_UNAVAILABLE
    and exit 2 and writes nothing, and shim status fails the same way.
    Install with uv tool or pipx.
  • One shim per client. A second installation, such as pipx beside uv tool, used to add a second full hook set, and shim revert then left one
    running while saying it was removed. Now install replaces another
    installation's hook and says so, revert removes every shim hook from any
    installation, and shim status and shim doctor name the other
    installation. A hook that wraps shim's command (| tee, && notify) is
    yours and left alone. shim install copilot no longer refuses a shim.json
    that another installation wrote. install and doctor are fast on large
    settings files again: on a 720 KB file, doctor took 0.35 s instead of 42 s.
  • shim config no longer drops settings without a word. On a settings
    file that did not parse, --only used to write a new one, discarding its
    [mode] (a user-prompt = "enforce" among it), [entities], [[custom]],
    [reveal], the ledger and markers. It now fails with SETTINGS_INVALID
    like every other flag and writes nothing. A pattern added beside --only
    turns CUSTOM on, where it used to stay off. --reset used to ignore
    --custom, --reveal, --ledger and --diet given with it; it now refuses
    every other option. On a refused file, --reset and
    --only report SETTINGS_REFUSED with the chmod fix instead of
    SETTINGS_CHANGED and Run the command again.
  • shim redact writes UTF-8 to a file or pipe whatever the locale. Under
    tr_TR.ISO8859-9 it used to transcode the file silently, and under Latin-1
    it crashed.
  • shim watch counts with your settings. A type you disabled is no longer
    counted, and an opt-in type you enabled and your custom patterns are. A
    settings file it cannot read stops it before the client starts, with
    SETTINGS_INVALID or SETTINGS_REFUSED.
  • shim watch no longer leaves the client behind. SIGTERM and SIGHUP
    reach the client, Ctrl-C reaches it once and escalates on repeats, and the
    proxy keeps its port until the client has exited, so no other process can
    take the port while the client still sends credentials to it. A client killed
    by a signal exits 128 plus the signal number, and --json says
    "status": "signalled".
  • shim watch on more networks, and more honest. The proxy follows
    HTTPS_PROXY (http:// proxies) and NO_PROXY, and uses SSL_CERT_FILE for
    a custom CA. Repeated request headers are forwarded as sent. A response of an
    unexpected shape is relayed whole instead of cut off. A field named
    signature is counted unless it is a thinking block's. A request read only in
    part, or still being measured when the report is written, says so. The section
    split never adds up to more than the request. With ANTHROPIC_BASE_URL set,
    the refusal no longer tells you to unset it, which would send that endpoint's
    credential to api.anthropic.com; its fix is now shim install claude.
  • shim watch starts without a DNS lookup. Starting the proxy looked up
    the name of 127.0.0.1, which on a machine with a slow or unreachable DNS
    resolver held shim watch before the client started, for more than 20
    seconds on GitHub's macOS runners. The proxy now only binds its port.

What to check after upgrading

  • MCP tools in Claude Code: a call whose arguments carry a finding is now
    refused by default. Set outbound = "warn", or "<tool name>" = "warn"
    for one tool, under [mode] to report it instead.
  • VS Code refusals: if you set outbound = "enforce" to refuse terminal
    commands, set executable-text = "enforce" now; file edits are
    local-write. outbound covers fetch_webpage, MCP and unknown tools. A
    local tool such as read_file is never refused.
  • A per-tool key on a local tool, such as Read = "enforce", now reports
    instead of masking.
  • Projects whose path holds an email address get a
    found EMAIL ... Not modified. line on each Read, Glob or Grep;
    local-tool = "observe" silences it.
  • Tool events fail closed in observe too, and a VS Code call shim cannot
    inspect is refused under the shipped defaults. A Write or Edit over 1 MB
    is refused.
  • The html diet no longer converts command output. A page you curl
    through Bash reaches the model as markup; an MCP or web tool's page is
    still converted.
  • Scripts that read output: --reset with another option now exits 2
    with OPTIONS_CONFLICT and the message --reset cannot be combined with other options.; shim status exits 2 with DETECTOR_UNAVAILABLE when the
    installed hook cannot run; BASE_URL_ALREADY_SET's fix is shim install claude; the
    cli.session.not_inspected, cli.doctor.hook_not_installed and
    cli.doctor.duplicate_hooks texts changed. Match code, never the text.
  • Downgrading: 1.2.1 reads a 1.2.2 settings file and ignores a
    local-tool key, but passes an event it cannot inspect through again, and
    its launcher lets a prompt through when no Python can run the hook.

Known gaps, fixed in 1.2.3

These were found by the same audit and are not fixed in 1.2.2. Each already
existed in 1.2.1.

  • Number-dense text can still reach the deadline. The phone scan reads a
    long line of short numbers, such as - 0 - 0 - 0 repeated, at about 6 s per
    100,000 characters on an M1, so a field of a few hundred thousand such
    characters reaches the 25-second deadline. It is then handled as any event
    shim could not inspect: withheld or refused, or, where shim can only report
    (a failed call's output, a VS Code result, a file attached with @),
    reported as not inspected while the model has it. The same text broken into
    lines is read in well under a second. 1.2.1 was slower still on it, and let
    the event through unmasked at the deadline.
    A list of phone numbers is read faster when numbers repeat, as they do in
    logs and exports: a repeated number is checked once. A megabyte of distinct
    valid numbers still takes about 5 s on an M1, each one checked by the phone
    library; it is inspected whole, inside the deadline.
  • Placeholder numbering (per-event numbering, single HTML scan). Numbers
    restart for every field and event, so two different addresses can both be
    <EMAIL_1> and one value can get several numbers. With diet = ["json", "html"], an IBAN split across two <span>s is masked only in part. Line
    numbers after a masked multi-line value, such as a PEM key, are off. A
    [reveal] tail can show the wrong digits for a percent-encoded value. Text
    shim already masked can be counted again as a secret the model wrote.
  • Keys and numbers in structured payloads. A JSON object key is never
    inspected, so an email address used as a key passes. A card, national ID or
    phone number sent as a JSON number passes. A secret under a structured key,
    such as {"password": "..."} or a {"name": "DB_PASSWORD", "value": "..."}
    pair, can pass unmasked. A PDF or image Read is scanned as text and can be
    rewritten whole.
  • Email addresses that swallow a path. In a tool result,
    /work/x@example.com/notes.txt becomes /<EMAIL_1>/notes.txt, a Google
    Drive folder named after an address hides the whole path, and an SSH remote
    git@github.com:org/repo.git is masked as an address.
  • Placeholders written back to disk. After a masked Read, the model's
    Write or Edit can put <SECRET_1> over the real value in the file, with
    no warning, even under local-write = "enforce".
  • Secret values in attributes and escaped JSON. In
    <add key="DbPassword" value="..."/> the placeholder lands on the attribute
    name and the value stays in clear, while shim reports it masked. A masked
    value swallows what follows a ;, & or ), such as connection-string
    options. A password inside escaped JSON, as AWS Secrets Manager prints it,
    passes in clear.
  • Session records. On an event shim could not inspect, and for a file
    attached with @, a value that only a custom pattern or an opt-in type
    matches is written into the record's file path as it is. A [reveal] tail
    reaches the record and the ledger. A value split by an invisible character
    is joined back together in the record. [entities] keyed by
    UserPromptSubmit is ignored for prompts.
  • Secret key names. Keys with the secret word glued on (PGPASSWORD,
    SECRETKEY, ACCESSTOKEN) and typed declarations
    (db_password: str = "...") are not detected. Nor is a value away from its
    key: Maven <password>, Kubernetes name:/value: lists, AWS SSM, and
    Terraform default =. A digit-only password under DB_PASSWORD or
    REDIS_PASSWORD passes.
  • Dropped records. Parallel hook events can drop session and ledger
    records without a word, so a summary undercounts. The ledger stops at its 5
    MB monthly cap silently. A ledger folder others can read drops every record,
    and the LEDGER_UNREADABLE fix points at shim doctor, which has no ledger
    check.
  • Connection strings with other schemes. The user and password in
    postgresql+asyncpg://, mysql+pymysql://, mariadb://, amqp://,
    smtp:// or ssh:// URIs pass in clear.
  • Context cues in code style. A Turkish tax ID after TAX_ID=, taxId,
    tax_number or vergiKimlikNo is missed. So is a phone number after
    phoneNumber, mobileNumber or contactPhone, or in a vCard
    TEL;TYPE=CELL: line.
  • Diagnostics that say the wrong thing. Some refusals name the wrong
    reason. A hook group where shim's hook sits beside another hook is refused
    with a fix asking you to make a file parse that already parses, and a
    group-writable file is blamed on another user. Doctor ignores
    CLAUDE_CONFIG_DIR when it looks for the plugin, counts disabled plugins,
    and crashes when two shim plugin keys are installed. A failed 0.2.0 settings
    move prints a traceback. [mode] default = "warn" silently turns off
    tool-result masking. Custom patterns stay listed while CUSTOM is off.
    SHIM_CONFIG naming a missing file silently runs the defaults.
  • Pattern coverage. PGP and DSA private key blocks, Stripe restricted keys
    (rk_live_), Slack app tokens (xapp-), GitLab runner and deploy tokens,
    Proxy-Authorization: Basic, an undelimited 9-digit SSN after ssn, and
    18- or 19-digit cards are not detected. An address that is only an address
    before percent-decoding is missed. A Turkish instruction-override line that
    starts like a commit subject (fix: ) is not flagged. shim config accepts
    some custom patterns that backtrack, or that only backtrack with
    ignore_case; the hook then hits its deadline and withholds the result.
  • shim audit and shim keys. shim audit --purge with a linked
    history.jsonl deletes the transcripts but keeps the prompt lines. shim audit says safe and exits 0 when it skipped lines. shim keys --json on
    an unreadable file prints a usage error instead of a JSON error.