Skip to content

fix(hooks): keep the session token out of curl's command line - #100

Merged
omartelo merged 1 commit into
mainfrom
fix/token-off-argv
Nov 8, 2026
Merged

omartelo merged 1 commit into
mainfrom
fix/token-off-argv

Conversation

@omartelo

@omartelo omartelo commented Nov 8, 2026

Copy link
Copy Markdown
Collaborator

What

Every shell hook (report-state.sh, report-tool.sh, report-session-start.sh, report-title.sh, report-touched.sh) posted to http://127.0.0.1:$LICH_PORT/<endpoint>?token=$LICH_TOKEN. The expanded token was an argument of curl, which /proc/<pid>/cmdline shows to every user on the machine, each time a hook fired.

The scripts now send the token as Authorization: Bearer <token>, fed to curl as a config line on stdin, the shape lich adopted in lichdotdev/lich#1067:

printf 'header = "Authorization: Bearer %s"\n' "$LICH_TOKEN" | curl ... -K - "$url"

printf is a shell builtin, so the token enters no argv. A -H argument would leak the same way as the query string.

Compatibility with a lich that does not accept the header

The plugin runs against whatever lich is installed, and every release before lichdotdev/lich#1067 answers 401 to a report whose token is only in the header. Each script reads the status of the first request (-w '%{http_code}', body to /dev/null) and, on a 401 alone, sends the same report again with ?token=. A 500 or a refused connection is not retried, so the existing "one request on a 500" tests hold unchanged. report-title.sh latches on whichever request lich took, so an older lich does not bring the transcript scan back on every tool call.

Cost, measured on report-touched.sh over 3 rounds of 500 runs against a loopback server: 5.2 to 6.6 ms a run before, 5.6 to 6.6 ms with the header accepted, about 10 ms on the fallback (one more curl).

Scope

  • The request stays inline in each of the five scripts. lich writes the scripts of a file-shipped harness one by one from a fixed list (antigravity.go, kiro.go, crush.go in internal/agentplugin), so a new sourced helper would not reach those installs under any released lich.
  • Windows runs the same .sh files through hooks/win-run.cmd and Git Bash, so it takes the same path. The repository has no PowerShell.
  • The Claude Code mods (hooks/*.js), opencode/lich.js and omp/lich.js report with an in-process fetch and run no process with the token; they are unchanged. agent-cards.js runs lich open with no token in its argv.

Tests

  • tests/hooks.test.mjs: every existing hook assertion now requires the token in the Authorization header and no token in the URL (assertHookHonoured). The in-process clients keep asserting ?token= through assertContractHonoured.
  • New, for each of the five scripts: run with a curl and a printf wrapper on PATH recording their argv against the stub server. One request, token in the header, token in no recorded argv line.
  • New, for each of the five scripts: run against a stub that answers 401 unless the URL carries ?token=. Two requests, the first with the header and a clean URL, the second the old shape with the same body.
  • New: report-title.sh once latches after a title an older lich took through the fallback.
  • The new tests failed before the change to the scripts (12 new failures, plus every existing hook test on the URL assertion) and pass after it.
  • node --test tests/*.test.mjs: 492 tests, 492 pass.
  • Live, hooks/report-touched.sh against a running lich that does not accept the header yet, with a wrapper recording each curl run: first run without the token in argv answered 401, second run with ?token= answered 204.
  • A header-only request with a wrong Bearer token against that same lich answers 401, which is the status the fallback keys on.

Every hook script posted to "http://127.0.0.1:$LICH_PORT/<endpoint>?token=$LICH_TOKEN",
so the expanded token was an argument of curl, readable by every user on the
machine through /proc/<pid>/cmdline each time a hook fired.

The scripts now send it as an Authorization: Bearer header fed to curl as a
config line on stdin (printf is a shell builtin, so the token enters no argv;
a -H argument would leak the same way). This is the shape lich adopted in
lichdotdev/lich#1067, which makes the listener accept the header.

A lich released before that answers 401 to a report whose token is only in
the header. The scripts read the status of the first request and, on a 401
alone, send the report again with ?token=, so a card keeps its state under any
lich. A 500 or a refused connection is not retried.
@omartelo
omartelo merged commit b8dceaf into main Nov 8, 2026
2 checks passed
@omartelo
omartelo deleted the fix/token-off-argv branch November 8, 2026 23:58
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