Repository navigation
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.