Skip to content

0.1.42: credentials on a command line stay on the machine; in-line variables are read; preflight stops guessing at steps it cannot read - #65

Merged
caiovicentino merged 4 commits into
mainfrom
client-0.1.42
Oct 16, 2026
Merged

caiovicentino merged 4 commits into
mainfrom
client-0.1.42

Conversation

@caiovicentino

Copy link
Copy Markdown
Member

Client 0.1.42, from a measured dogfooding log: 687 commands of one Claude Code session run through ekbasis-claude-hook 0.1.41 in log mode (shell guard and preflight on, hosted API).

Security: credentials typed on a command line stay on the machine

Until 0.1.41 the command line went verbatim into the state the client sends (hook, CLI, MCP tools, Python API) and into the hook's log in log mode. A key typed on it (printf '%s' 'vck_…' > f, export TOKEN=…, curl -H "Authorization: Bearer …", https://user:token@…, --password=…) reached the server and the disk. In that session's log it happened once.

ekbasis.redact.redact_line now runs on every state Ekbasis.ask sends, on feedback notes, on the hook's log record, reasons and messages, and on the monitor's log. It replaces credential-looking values with placeholders made of [A-Za-z0-9_] (REDACTED_1, vck_REDACTED_2; the same value gets the same placeholder), and keeps quotes, spaces, redirections and paths. The parsed line keeps the same shape (tested).

  • What it replaces:
    • tokens with a known prefix (sk-ant-, sk-, sk_live_, vck_, hf_, ghp_/gho_/ghs_, github_pat_, xox?-, AKIA/ASIA, glpat-, npm_, pypi-AgE, AIza, JWTs);
    • PEM private key bodies;
    • Authorization, X-Api-Key, X-Auth-Token and Cookie header values;
    • passwords in URLs;
    • --password, --token, --api-key, --secret values;
    • values of assignments and JSON/YAML keys named like token, secret, passw, api_key, auth (not author), credential or private_key, when the value has 8+ characters and is not a number, a boolean or a path;
    • as a last resort, long random-looking words.
  • What stays: variables and substitutions ($TOKEN, $(cat key)), paths, git refs, commit ids, sha256 sums and UUIDs.
  • Log and switch: the hook's log records "redacted": n, and EKBASIS_REDACT=0 turns redaction off.
  • Docs: README and docs/SECURITY.md now say what leaves the machine (the redacted command line; a listing of names, sizes and keyed fingerprints, never contents; for git, the repository's state) and the limits of redaction.
  • Measured on the 687 logged lines: redaction changes none of them (none still holds a credential). On 14 of them with made-up credentials added (a key written to a file, a Bearer token, an exported token), each run through the hook with and without redaction against the hosted API (one line at a time, at most 2 requests in flight):
    • all 14 got the same decision and the same kinds of warning;
    • the probabilities moved by up to 17 points, about as much as repeated runs of an unchanged line move;
    • no redacted log held the credential.

The guards read a variable set earlier on the same line

The git guard and the shell guard (shell.resolve, used by parse and by the hook's line reader) read $NAME / ${NAME} with its value when the line sets NAME before, to a known value: G=/repo; git -C "$G" status, D=build; rm -rf "$D".

The assignment must be:

  • a command made only of NAME=value words (a literal, or names already known);
  • run unconditionally at the top level;
  • the only place the line sets NAME.

Everything else is still not guessed and stays "cannot foresee", as before:

  • a variable from the environment;
  • one set twice or used before it is set;
  • one that is exported, read, or set by a loop, a condition, a pipeline, a subshell or a command substitution;
  • every variable of a line with eval, source or a function.

The shell guard's state names the values it used.

Preflight does not guess at a step it cannot read

  • Inline code and other machines. A step that runs code given inline to an interpreter (python3 -c, python3 - <<'EOF', node -e, …) or runs on another machine (ssh, scp/rsync to or from a host) is never named as the failing step. The other steps are still judged. When none of them changes anything, the line is "cannot judge": silent, or one warning with EKBASIS_PREFLIGHT_FAIL_CLOSED=1.
  • Why per step and not the whole line. Making the whole line "cannot judge" lost 3 real catches. In each, preflight had said "step 3 python3 assets_src/make_chart_jev.py likely fails, step 2 (an inline edit) already ran", and the session's transcript shows exactly that failure.
  • Loops on one line. Root cause of "for f in …; do …; done is likely to fail (checked in code)": one-line chains were cut at ; into commands named for/do/done/case/esac. Code then found them "not installed" and decided that the step fails. Such lines are now "cannot judge", as scripts already were.
  • Two more code facts that misfired, both fixed:
    • a redirection into a folder that an earlier step creates (mkdir -p out; … > out/log);
    • a command given by its path (./node_modules/.bin/tsc), which code took as "not installed".

Replay: 0.1.41 and 0.1.42 on the same commands and state

0.1.41 0.1.42
lines flagged 299 137
preflight warnings 171 52
… on commands that really failed (the session ran them all) 6 3
shell guard "could not foresee" 64 19
git guard "cannot foresee" 76 15
shell guard "may lose" / overwrites (judged now, not cannot-foresee) 45 / 1 57 / 4

What disappeared: 246 flags on 221 lines.

  • 56 for one-line loops, 53 for inline code, 12 for remote steps;
  • 6 code facts that were wrong;
  • 118 where a variable is now read;
  • 1 model answer that moved under the threshold, on a command that had succeeded.

True-positive check:

  • Of the disappeared flags, those on commands that really failed are 3 preflight warnings that had named an inline step that did not fail, 2 lines that are still asked, and 1 read-only line.
  • Each line where a flag went away because a variable was now read, and where content could be lost, was run again with the work at stake put back (a dirty file, a repository with uncommitted work, dry-run outputs): 0.1.42 flags 15 of 15.

Known follow-up: 10 new preflight warnings, all on commands that succeeded. They sit on lines where 0.1.41's git guard gave up first and preflight never ran. Their cause: preflight's listing does not follow a cd from an earlier step (an issue will be opened).

Checks

  • Python 3.12: 393 passed, 30 skipped, including 30 new tests: in-line variables, preflight from sanitized log lines, every redaction pattern, no false redaction of paths/hashes/branches/sums, and that the line keeps its shape.
  • Docker framework tests pass locally; the PostgreSQL ones run in CI.
  • Python 3.9: the new and touched test modules pass.
  • The plugin copy is in sync.
  • Version 0.1.42 is set in _version.py, pyproject.toml, plugin.json and server.json.
  • The TypeScript SDK is unchanged; it has no command-line logic.

🤖 Generated with Claude Code

Caio Vicentino and others added 4 commits October 16, 2026 16:32
…longer guesses at steps it cannot read

From a measured dogfooding log (687 commands of one Claude Code session, ekbasis-claude-hook 0.1.41 in log mode).

The git guard and the shell guard read $NAME / ${NAME} with its value when the line itself sets NAME earlier to a
known value (shell.resolve): a command made only of NAME=value words, at the top level, unconditional, the only place
the line sets NAME. Anything else stays "cannot foresee" as before (environment, set twice, used before set,
export/read/for/local, loops, conditions, pipelines, subshells, command substitution, eval/source/functions). A
resolved word keeps the lexer's quoted flag, so `$S/hooktest.*` still expands as a pattern; the shell guard's state
names the values it used.

Preflight: a step that runs code given inline to an interpreter (python -c, a program on stdin or in a
here-document, node -e, ...) or runs on another machine (ssh, scp/rsync host:) is never named as the failing step
(Step.opaque); the other steps are still judged, and when none of them changes anything the line is "cannot judge"
(silent unless EKBASIS_PREFLIGHT_FAIL_CLOSED=1). Root cause of "for ... do ... done ... likely to fail (checked in
code)": one-line chains were cut at ; into commands named for/do/done/case/esac, which shell_facts found "not
installed"; loops and conditions on one line are now "cannot judge", as in scripts. Two more code facts that
misfired in the replay: a redirection into a folder an earlier step creates (mkdir -p out; ... > out/log), and a
command given by its path (./node_modules/.bin/tsc) taken as "not installed".

Replay through 0.1.41 and 0.1.42, hosted API, same state: lines flagged 299 -> 137; preflight warnings 171 -> 52
(on commands that really failed 6 -> 3, the 3 kept name the failing step); shell guard "could not foresee" 64 -> 19;
git guard "cannot foresee" 76 -> 15. Lines whose flag went away because a variable was read, re-run with the work at
stake put back: 15 of 15 flagged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…the machine or land in the logs

The command line went verbatim into the state the client sends (hook, CLI, MCP tools, Python API) and into the hook's
log: a key typed on it (printf 'vck_...' > f, export TOKEN=..., curl -H "Authorization: Bearer ...",
https://user:token@..., --password=...) reached the server and the disk. ekbasis.redact.redact_line now runs on every
state Ekbasis.ask sends, on feedback notes, on the hook's log record, reasons and messages, and on the monitor's log.

It replaces credential-looking values with placeholders made of [A-Za-z0-9_] (REDACTED_1, vck_REDACTED_2; the same
value gets the same placeholder): known prefixes (sk-ant-, sk-, sk_live_, vck_, hf_, ghp_/gho_/ghs_, github_pat_,
xox?-, AKIA/ASIA, glpat-, npm_, pypi-AgE, AIza, JWTs), PEM private key bodies, Authorization/X-Api-Key/X-Auth-Token
and Cookie header values, URL passwords, --password/--token/--api-key/--secret values, values of assignments and
JSON/YAML keys named like token/secret/passw/api_key/auth(not author)/credential/private_key (8+ characters, not a
number, boolean or path), and long random-looking words as a last resort. Variables and substitutions ($TOKEN,
$(cat key)), paths, git refs, commit ids, sha256 sums and UUIDs stay. Quotes, spaces, redirections and paths stay
where they are, so the parsed line has the same shape (tested). The hook's log records "redacted": n.
EKBASIS_REDACT=0 turns it off.

Measured: on the 687 logged lines it changes none (no credential left in that log; no path, ref or hash touched). On
14 of them with made-up credentials put on them, run through the hook with and without redaction on the hosted API
(one line at a time, at most 2 requests in flight): the same decision and the same kinds of warning for all 14,
probabilities within 17 points; the redacted logs never held the credential. README and docs/SECURITY.md say what
leaves the machine and the limits of redaction.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…) and of which tools are installed

On Linux the shell guard lists a path under the folder as relative, on macOS (/var -> /private/var) as absolute: the
test now accepts either. The one-line loop test checks that no reserved word is called "not installed", not that no
command is (ps is missing from slim images). Full suite in python:3.12-slim: 389 passed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ls restored in the answers

Ekbasis.ask redacted the state but not the questions, which can carry runtime values: recap() puts the caller's
rules into `instructions`, and the MCP predict_consequences tool takes caller-written questions and criteria. Every
string of every question is now redacted before sending (instructions, criteria labels and descriptions; "type" is
untouched). Labels are redacted in one pass per question so distinct labels stay distinct, and a redacted label is
mapped back to the caller's in the answer (value, probabilities, calibrated probabilities). Test: a recap()'d
question with a key in the rules and an MCP-style choice question with keys in a label and a description; nothing
leaves, and the answer carries the caller's label. CHANGELOG and SECURITY say so.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@caiovicentino
caiovicentino merged commit 85fc3fe into main Oct 16, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant