Skip to content

Repository files navigation

thlibo

From Greek θλίβω — to press, crush, compress.

AI tool output compressor for local coding agents. When Claude Code runs git status, npm install, cargo test, or a verbose log dump, thlibo intercepts the Bash-tool invocation, runs the real command, and hands the AI a compressed version of the output — preserving every load-bearing detail and dropping the noise.

Saves 60-99% of tokens on common dev commands without the AI knowing the difference.

Uses the same PreToolUse+updatedInput pattern described in Claude Code's hooks documentation: rewrite the Bash command before it runs so the subprocess stdout is already compressed when the tool result is captured. Same token savings, no proxy, no API-wire tampering.

 git diff HEAD~5        68821 bytes      ──thlibo──►    929 bytes     (98.6%)
 npm list 200 deps      ~6200 bytes      ──thlibo──►   ~600 bytes     (90%)
 cargo test failing     ~4800 bytes      ──thlibo──►   ~480 bytes     (90%)
 log file 500 lines    ~20000 bytes      ──thlibo──►  ~1500 bytes     (92%)

Methodology

The savings figures on the marketing page and in this README are measured in tokens — the unit you actually pay for — not raw bytes:

  • Without thlibo = what Claude Code consumes natively. Text outputs at ~4 chars/tok; PDFs at ~2,250 tok/page (page render + extracted text).
  • With thlibo = bytes returned by the processor, ÷4.

A 7-day log won't fit in any context window (Claude's is 200k tok). The "without thlibo" figure for those rows is what the bytes would tokenise to — in practice, without thlibo you wouldn't be able to ask the model about that data at all.

Commands reproducible via go test ./internal/middleware/... -run TokenSavings. PDF + log fixtures embedded with inferd's 300M-param embed model; top-percentile windows surfaced by k-NN density (CORDON_MAX_WINDOWS=5000).

Works with Claude Code (Bash + PowerShell + Read + Write/Edit hooks), Codex CLI (PostToolUse decision: block), Cursor IDE (preToolUse updated_input command + file-read rewrite; no MCP), GitHub Copilot CLI (preToolUse modifiedArgs command rewrite + postToolUse modifiedResult output compression), and VS Code Copilot (1.111+, currently Insiders) — the Copilot hook file thlibo installs is auto-discovered by VS Code too, and the hook scripts speak both wire formats.

Want to see how much it's saving you? thlibo can emit optional OpenTelemetry metrics + events to a collector you run — off by default, content-free, one env var to enable. See Monitoring.


How it works

thlibo is one binary plus PreToolUse hooks. Inference runs in a separate sidecar daemon, inferd, which thlibo's installer detects-or-installs alongside itself.

Claude Code: about to run `git status`
      │
      ▼
 PreToolUse hook (bash / powershell)
      │  ask: thlibo rewrite "git status"
      ▼
 thlibo (CLI)
      │  registry lookup on argv[0] == "git"  →  wrap it
      │
      ▼
 hook emits: updatedInput = "thlibo exec -- git status"
      │
      ▼
 Claude Code runs the rewritten command
      │
      ▼
 thlibo exec runs the real git, captures stdout
      │
      ▼
 middleware: fast-path regex → git-filter (native Go)
             OR router call → inferd sidecar → Gemma 4 processor
      │
      ▼
 compressed stdout, original stderr, original exit code
      │
      ▼
 Claude Code: tool_output = compressed bytes
      │
      ▼
 Model never sees the original — only the summary.

The deterministic filters (git-filter, npm-filter, cargo-filter, pytest-filter, go-test-filter, ndjson-filter, stacktrace-filter, lint-filter, trivy-filter, har-filter, mhtml-filter, pdf-filter) are native Go — compiled into the binary, run in-process, no inferd and no Python (ADR 0010, ADR 0015). PDFs included: pdf-filter reads them in-process, roughly 99× faster than the Python path and with less text mangling, using the document's own structure tree when it has one. (That figure is an end-to-end aggregate over five real corpus documents, 51.3 s → 0.52 s — full table and method in ADR 0015. It was measured once against Python during the port and is not re-derived by the in-tree benchmarks, which are Go-only.) pdf-to-md remains installed for the one thing native Go can't do — a scanned, image-only PDF has no extractable text, so its pages get rasterized and handed to inferd's Gemma vision model for OCR (see below). Prompt processors (compress, casefolder, shorthand) dispatch through inferd for LLM-driven summarisation of unfamiliar output. cordon-filter (semantic anomaly surfacer, native Go) embeds windows via inferd and surfaces the rare ones — it runs automatically as a fallback when ndjson-filter over-collapses a log (e.g. an access log where every line shares the same level+msg), restoring the outliers a signature-based collapse would hide.

Everything runs on your machine. No network calls during inference, no telemetry, nothing leaves localhost — unless you explicitly opt in to OpenTelemetry metrics, which emit content-free usage/savings stats to a collector you configure.


Install

One-liner (Unix)

curl -fsSL https://raw.githubusercontent.com/3rg0n/thlibo/main/scripts/install.sh | bash

Pin to a specific version:

curl -fsSL https://raw.githubusercontent.com/3rg0n/thlibo/main/scripts/install.sh | THLIBO_VERSION=v0.11.5 bash

One-liner (Windows PowerShell)

irm https://raw.githubusercontent.com/3rg0n/thlibo/main/scripts/install.ps1 | iex

Or pinned:

$env:THLIBO_VERSION='v0.11.5'; irm https://raw.githubusercontent.com/3rg0n/thlibo/main/scripts/install.ps1 | iex

Both installers:

  1. Download the platform-matching release tarball/zip from GitHub.
  2. Verify SHA-256 against SHA256SUMS published in the same release.
  3. Extract thlibo into ~/.local/bin (Unix) or %LOCALAPPDATA%\thlibo\bin (Windows).
  4. On Windows, add the install dir to the User PATH (no admin).
  5. Run thlibo install — writes Claude Code hooks, mirrors processors, probe-or-installs the inferd sidecar.

Skip step 5 with THLIBO_SKIP_INSTALL=1 if you want to inspect the binary before running configure.

The one-liner wires Claude Code by default. To also install the Codex, Cursor, or Copilot hooks through the one-liner (it can't take flags), set an env var:

# Unix
curl -fsSL https://raw.githubusercontent.com/3rg0n/thlibo/main/scripts/install.sh | THLIBO_CODEX=1 THLIBO_CURSOR=1 THLIBO_COPILOT=1 bash
# Windows
$env:THLIBO_CODEX=1; $env:THLIBO_CURSOR=1; $env:THLIBO_COPILOT=1; irm https://raw.githubusercontent.com/3rg0n/thlibo/main/scripts/install.ps1 | iex

(Equivalently, run thlibo install --codex --cursor --copilot yourself afterward.)

Prerequisites for running

  • Python 3.8+ — optional. Every built-in filter (git, npm, cargo, go test, pytest, lint, trivy, ndjson, stacktrace, pdf, cordon) is native Go and needs no Python. Python is only required for scanned PDFs (pdf-to-md rasterizes their pages for OCR; born-digital PDFs don't touch it), plus any of your own .py processors.
  • jq — the Claude Code hook shell script needs it. Install via your package manager or winget install jqlang.jq on Windows.
  • git — for git-related compression you probably have it already.

On Windows you also need bash on PATH so Claude Code can execute the PreToolUse hook script. Git Bash (bundled with Git for Windows) is sufficient — if git works in your shell, you're fine. WSL is also an option.

From source

git clone https://github.com/3rg0n/thlibo.git
cd thlibo
mkdir -p bin

# Unix:
go build -o bin/thlibo ./cmd/thlibo

# Windows (note the .exe):
go build -o bin/thlibo.exe ./cmd/thlibo

Copy the binary somewhere on your $PATH (e.g. ~/.local/bin/) and run thlibo install.

Run the installer

thlibo install

This does five things:

  1. Mirrors the embedded built-in processors to ~/.thlibo/processors/ (script processors need an on-disk directory to chdir+exec into).
  2. Writes the Claude Code hook scripts to ~/.thlibo/hooks/.
  3. Merges PreToolUse/Bash, PowerShell, Read, and Write/Edit hook entries into ~/.claude/settings.json (preserves all your existing hooks and settings verbatim — idempotent).
  4. Probe-then-delegate inferd: if inferd is already running, uses it; if installed but stopped, starts it; otherwise downloads the latest inferd release and runs inferd's bundled installer (which registers inferd's own per-platform autostart).
  5. Reports the plan as it goes (use --dry-run to see without touching anything).

No admin/root required, on any OS — for thlibo or inferd. All three inferd autostart mechanisms are per-user and need no elevation: a launchd LaunchAgent (macOS), a systemctl --user unit (Linux), and a Startup-folder shortcut (Windows). A fresh install goes from nothing to a running, login-autostarting daemon with no manual step.

Install flags

--dry-run             Report the plan, don't apply it.
--processors-dir      Override ~/.thlibo/processors.
--hook-dir            Override ~/.thlibo/hooks.
--settings            Override ~/.claude/settings.json.
--skip-hook           Mirror processors only; don't touch Claude Code.
--skip-inferd         Don't probe / install the inferd sidecar.
--inferd-version      Pin inferd to a specific tag (default: latest).
--codex               Also install the Codex CLI PostToolUse hook.
--cursor              Also install the Cursor IDE preToolUse hook.
--copilot             Also install the GitHub Copilot CLI hooks.

With --codex, thlibo appends an inline [[hooks.PostToolUse]] block (matcher ^Bash$) to ~/.codex/config.toml and sets [features] hooks = true in the same file — written inline, not to a separate hooks.json, because Codex warns and hides hooks when one config layer mixes both representations (#170); any stale thlibo hooks.json entry from an older install is removed. Codex gates command hooks behind a trust step: after install, run /hooks inside Codex, review the thlibo hook, and approve it — until you do, Codex sees the hook but won't run it (compression stays off). The installer prints this reminder.

With --copilot, thlibo writes ~/.copilot/hooks/thlibo.json plus four hook scripts (a Bash + PowerShell pair for each event). Copilot reads every *.json in that directory and each tool owns its own file, so thlibo's never collides with another tool's. Two hooks are installed: a preToolUse hook that rewrites a shell command's input (git statusthlibo exec -- git status) via modifiedArgs, and a postToolUse hook that replaces any verbose tool's output with a compressed version via modifiedResult. Copilot's preToolUse is fail-closed, so the hook only ever allows — it never blocks a tool call; postToolUse is fail-open. Restart Copilot CLI to load the hooks.

VS Code Copilot (1.111+, currently Insiders) comes along for free. VS Code reads agent hooks from ~/.copilot/hooks/ too, so the same thlibo.json is auto-discovered — no extra install step. VS Code uses a different wire format than the CLI (the Claude-Code envelope: tool_input / hookSpecificOutput.updatedInput, and an observe-only postToolUse), so the hook scripts detect which host is calling and reply in the matching format. On VS Code, shell output is compressed via the preToolUse command-wrap (its postToolUse can't replace output — same limitation as Claude Code). No --vscode flag is needed; --copilot covers both.

The model GGUF (~5.1 GB Gemma 4 E4B) is downloaded by inferd on first inference request, into a shared per-platform model store (~/.local/share/models/ on Linux, ~/Library/Application Support/models/ on macOS, %LOCALAPPDATA%\models\ on Windows). thlibo doesn't manage the model — that's inferd's job.

Install footprint — what gets modified

thlibo install touches these paths and nothing else. Every hook and skill file is SHA-stamped: if you've edited one, the new version lands at <path>.new and your edit is preserved.

Path What
~/.local/bin/thlibo (Unix) · %LOCALAPPDATA%\thlibo\bin\ (Win) the thlibo binary (placed by the one-liner installer; on Windows the dir is added to the User PATH)
~/.thlibo/hooks/ the six PreToolUse hook scripts (Bash + PowerShell variants of exec / read / write)
~/.thlibo/processors/ the embedded built-in processors, mirrored to disk (your own processors here are left untouched)
~/.claude/settings.json only the hooks block — PreToolUse matchers for Bash, PowerShell, Read, Write, Edit are merged in; every other key and hook you have is preserved verbatim
~/.claude/skills/caselog/ the /caselog skill
inferd binary + backends/ libs ~/.local/bin (Unix) · %LOCALAPPDATA%\inferd\ (Win), via inferd's installer
inferd autostart LaunchAgent (macOS) · systemctl --user unit (Linux) · Startup-folder shortcut (Windows)
~/.codex/config.toml only with --codex — inline [[hooks.PostToolUse]] block + [features] hooks = true appended (a stale hooks.json entry, if any, is removed)
~/.copilot/hooks/thlibo.json + four hook scripts only with --copilot

On macOS the one-liner installer (install.sh) also strips the Gatekeeper quarantine attribute (xattr -d com.apple.quarantine) from the downloaded thlibo binary so it runs without a "blocked" dialog.

thlibo install does not modify any other settings.json keys — in particular it does not touch skipDangerousModePermissionPrompt, skipWebFetchPreflight, or any permission/safety setting.

Two behaviors worth knowing

These are intentional and named in THREAT_MODEL.md (findings MA-2 and MA-6); calling them out here so they aren't a surprise:

  1. The hooks auto-allow their own rewrites. When a PreToolUse hook rewrites a command (or substitutes a compressed file for the Read tool), it emits permissionDecision: "allow" for that single, thlibo-rewritten invocation — so Claude Code doesn't re-prompt for the thing thlibo just produced. It only ever allows the rewritten form it emitted; it does not blanket-allow other commands. The rewritten command is visible to you and logged by thlibo exec.
  2. The PreToolUse hook is persistent. It's a one-time install but the hook stays in ~/.claude/settings.json and intercepts matching tool calls in every future Claude Code session until you run thlibo uninstall. cat ~/.claude/settings.json to see it.

Uninstall

thlibo uninstall            # remove hooks + scripts; leave ~/.thlibo
thlibo uninstall --purge    # also delete ~/.thlibo (processors + state)

uninstall removes the Claude Code hook entries and thlibo's own ~/.copilot/hooks/thlibo.json (leaving any other tool's hook file untouched). It does not unpick the inline hook thlibo appended to ~/.codex/config.toml or the Cursor hooks.json entry — remove those by hand if you installed them.

Inferd is left running because other tools may use it. To remove inferd separately, use inferd's own uninstaller — see inferd's docs.


Customise

Drop in your own processor

mkdir -p ~/.thlibo/processors/my-tool
cat > ~/.thlibo/processors/my-tool/processor.yaml <<'YAML'
name: my-tool
type: script
entry: run.py
commands:
  - my-custom-cli
match: "^Running: "
description: >
  Compresses my-custom-cli's verbose progress output to a summary line.
YAML
cat > ~/.thlibo/processors/my-tool/run.py <<'PY'
import sys
for line in sys.stdin:
    if not line.startswith("Progress:"):
        sys.stdout.write(line)
PY
chmod +x ~/.thlibo/processors/my-tool/run.py

Restart your shell (or re-run thlibo install) and the hook picks it up on the next Claude Code invocation. User processors with the same name as a built-in override the built-in.

Script processor vs prompt processor

Script Prompt
Descriptor processor.yaml + entry file processor.md (YAML frontmatter + body)
Speed ~10 ms ~200-800 ms (inferd round-trip)
Determinism Always the same output for the same input Model-dependent
When to use Fixed-format output (git, npm, cargo, known log schemas) Unfamiliar output; stack traces; arbitrary logs
Inferd needed? No Yes

Built-in processors

Name Type Handles
git-filter native git status, git diff, git log
npm-filter native npm, npx, pnpm, yarn
cargo-filter native cargo build, cargo test, cargo clippy
pytest-filter native pytest output
ndjson-filter native structured-log streams
stacktrace-filter native Python / Go / Rust / Java / Node stack traces
lint-filter native clang, gcc, clippy, eslint, golangci-lint, gosec, shellcheck, flake8, ruff, mypy, rubocop, stylelint. Auto-wraps gosec, staticcheck, golangci-lint, shellcheck (not go/make/docker — see below)
go-test-filter native go test -v / go test -json — keeps failures + package tally, drops passing-test noise. Auto-wraps go test (only that subcommand)
trivy-filter native trivy scan output — keeps findings by severity, drops the per-target boilerplate
cordon-filter native Semantic anomaly surfacer for logs: windows the input, embeds each window via inferd, ranks by k-NN distance and keeps the outliers. Fires automatically when ndjson-filter over-collapses a stream (so a repetitive access log doesn't compress down to one uninformative row), or on request in a chain. Never model-selected
har-filter native .har (HTTP Archive) captures — content-matched, not command-wrapped. One redacted line per request (METHOD status url (mime size ms)); drops static assets + non-text bodies + timing plumbing; redacts query-string secrets, auth headers, POST-body creds, JWTs + long tokens (typically ~99% smaller)
mhtml-filter native .mhtml/.mht saved-web-page archives — content-matched. Extracts the article HTML from the MIME bundle → Markdown (headings, lists, links, code/pre, tables, images as ![alt](src) refs); drops the base64-embedded images/CSS/scripts that are ~90% of the file (typically ~98% smaller)
pdf-filter native PDF → GitHub-flavored markdown, in-process, no Python — content-matched on %PDF-. Four tiers, best evidence first: a Tagged PDF's own /StructTreeRoot (headings, tables and reading order stated by the producer), native text extraction, geometry table detection, and a [scanned page N] placeholder handing image-only pages to the OCR path. Also emits document metadata + bookmark outline; drops running headers/footers, promotes numbered headings, rejects invisible layout grids so a slide deck's positioning frames aren't reported as tables. Encrypted documents pass through untouched (typically ~97% smaller)
pdf-to-md script The OCR path for scanned PDFs — rasterizes pages for inferd's Gemma vision model (ADR 0009). Born-digital PDFs go to pdf-filter instead
shorthand prompt LLM-facing prose compression (SKILL.md, CLAUDE.md, system prompts)
compress prompt Generic verbose output, fallback
casefolder prompt Stack traces, error logs, crash output

go is matched per-subcommand. go test wraps (→ go-test-filter), but go build / go run / go vet / go generate do notgo's argv[0] is multiplexed, so a command_prefixes: ["go test"] rule wraps exactly the test verb and leaves the others alone. Intentionally not wrapped at all (a recorded decision): make, docker build — they emit too many output shapes for a single filter to compress safely.


Check it's working

# Inferd sidecar is running
# Linux:    systemctl --user is-active inferd
# macOS:    launchctl list | grep io.inferd.daemon
# Windows:  sc.exe query inferd-daemon

# Hook is registered in Claude Code
grep -c thlibo ~/.claude/settings.json
# Expected: 5+ (Bash + PowerShell + Read + Write + Edit matchers)

# Direct test of the rewrite path
thlibo rewrite 'git status'
# Expected stdout: "<thlibo-path> exec -- git status"
# Expected exit:   0

# Direct test of the exec path
thlibo exec -- git diff HEAD~5 | wc -c
# Expected: far fewer bytes than `git diff HEAD~5 | wc -c` alone.

If the hook silently doesn't fire in a Claude Code session, check the debug log:

claude --debug-file /tmp/claude.log 'Run git status via Bash'
grep -E 'Hook|PreToolUse|updatedInput' /tmp/claude.log

You should see Hook PreToolUse:Bash (PreToolUse) success: with a updatedInput object pointing at thlibo exec -- .... If you don't, the hook script isn't being invoked — usually a PATH issue (the hook needs thlibo and jq on Claude Code's Bash PATH).


Output streams

thlibo uses stdout and stderr separately and on purpose:

  • stdout — only the compressed (or pass-through) bytes the AI client should consume. Always safe to capture.
  • stderr — diagnostics: reduction summaries, fallback reasons ("backend unavailable; emitting original"), and the occasional background update-available banner.

Don't merge them with 2>&1 when feeding output to an AI client or to thlibo itself. The update banner and other stderr lines are human diagnostics, not data — merging them risks polluting the captured payload. Examples:

# Good: only the compressed payload reaches the AI.
thlibo exec -- git diff HEAD~5 > diff.compressed

# Good: keep stderr visible for the human in the terminal,
# stdout clean for the pipe.
thlibo exec -- npm install | other-tool

# Avoid: merges human diagnostics into the data stream.
thlibo exec -- git diff 2>&1 | other-tool

If you must capture stderr for debugging, route it to its own file (2>thlibo.err) instead of merging.


Disable without uninstalling

Temporarily stop compressing without removing anything:

# Set this in your shell profile or Claude Code environment:
export THLIBO_DISABLED=1

Every hook honours this flag and exits passthrough immediately.


Monitoring (OpenTelemetry)

thlibo can emit OpenTelemetry metrics and events so you can see how much it's saving — for a single developer, or aggregated org-wide. It mirrors Claude Code's monitoring model: off by default, enabled by one flag, configured through the standard OTEL_* environment variables, and pointed at your own collector. thlibo only emits; you own the collector, storage, and dashboards (Grafana, Honeycomb, a raw OTLP receiver — your choice). See ADR 0011.

# Enable + point at a collector (recommended: one on localhost).
export THLIBO_ENABLE_TELEMETRY=1
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf   # or grpc
# Optional: label your data for org rollups.
export OTEL_RESOURCE_ATTRIBUTES=team.id=platform,department=eng

Config is read from the process environment (your shell/profile), not from the AI client — Claude Code doesn't pass its own OTEL_* to hook subprocesses, so thlibo's telemetry is configured independently.

Variable Meaning Default
THLIBO_ENABLE_TELEMETRY master enable unset → off
OTEL_METRICS_EXPORTER otlp | console | none otlp
OTEL_LOGS_EXPORTER otlp | console | none otlp
OTEL_EXPORTER_OTLP_ENDPOINT collector endpoint http://localhost:4318
OTEL_EXPORTER_OTLP_PROTOCOL http/protobuf | grpc http/protobuf
OTEL_EXPORTER_OTLP_HEADERS auth headers
OTEL_SERVICE_NAME / OTEL_RESOURCE_ATTRIBUTES resource / org labels thlibo / —

Metrics (namespaced thlibo.*): invocations, bytes.processed, bytes.saved, compression.ratio, dispatch.duration, fallbacks. Savings are reported in exact bytes (thlibo.bytes.saved = bytes in − bytes out); thlibo has no tokenizer, so convert bytes → tokens → cost in your dashboard. One event (thlibo.compression) is emitted per invocation with { processor, path, outcome, bytes_in, bytes_out, duration_ms }.

What is never emitted: tool output, prompts, shell commands, or file paths — only sizes, counts, durations, and fixed enum labels. There is no content-capture opt-in. Built-in processor names appear verbatim; user processor names are redacted to "custom".

Cost when off: zero. With THLIBO_ENABLE_TELEMETRY unset, no SDK is constructed and nothing runs on the hook path.

Latency when on: thlibo's hook subcommands are short-lived (they exit per tool call), so telemetry is force-flushed on exit within a fixed 2-second cap. Against a localhost collector that's microseconds; a misconfigured/unreachable remote endpoint makes each call wait up to 2 s before dropping the batch — telemetry is always best-effort and never blocks or breaks the AI client. Run a collector on localhost.

Setup notes (two gotchas)

Both follow standard OpenTelemetry behaviour, but they bite the first time:

  1. Set OTEL_* in your shell profile, not just the AI client's env. When thlibo runs from an AI client's hook (Claude Code's Bash-tool PreToolUse hook, for example), the client does not pass its own OTEL_* into hook subprocesses — Claude Code deliberately strips them. THLIBO_ENABLE_TELEMETRY set in the client env still reaches thlibo, but the OTEL_EXPORTER_OTLP_* endpoint/protocol vars won't. Export them from your ~/.bashrc / ~/.zshrc / PowerShell $PROFILE (or a system-wide env) so every thlibo subprocess inherits them.
  2. A plaintext local collector needs an http:// endpoint (or INSECURE). With no endpoint set the OTLP exporter defaults to TLS, which fails silently against a plaintext collector (thlibo fails open and drops the data). Either give an explicit OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318, or set OTEL_EXPORTER_OTLP_INSECURE=true.

A collector is a shared sink: thlibo, the AI client, and any other instrumented tool can all point at the same one. Separate the sources at query time by service.name (thlibo sets thlibo) or by the metric-name prefix (thlibo.* vs claude_code.*). thlibo only ever emits its own thlibo.* signals.


Security model

  • All-local at runtime. No network calls during inference. The inferd sidecar listens only on a Unix domain socket / Windows named pipe / loopback TCP — never on a public interface.
  • One network touch per host: model download. Inferd fetches the GGUF on first request and verifies a SHA-256 published in inferd's own release. After that, the daemon is offline.
  • User-scoped. On Unix, the inference socket is mode 0660 owned by group inferd-users (or user-only when the group doesn't exist); admin socket is 0600 owned by the daemon user. On Windows, the pipe ACL grants the current user only; Everyone is excluded.
  • No elevation, anywhere. thlibo install runs entirely under your user account, and so does inferd's: all three autostart mechanisms are per-user — LaunchAgent (macOS), systemctl --user unit (Linux), Startup-folder shortcut (Windows). No admin/root at any point.
  • Fallback on every error. If anything in the compression path fails — inferd unreachable, script crashes, processor times out, malformed response — the original output is returned unchanged. The AI never sees a broken intermediate state.
  • Hook auto-allows rewritten commands. By design, the PreToolUse hook returns permissionDecision: allow for every Bash command it rewrites so users aren't re-prompted for the same action. That means: once installed, every Bash tool-call that matches the hook matcher runs through thlibo's rewrite without a Claude Code permission prompt. See THREAT_MODEL.md finding #15 for the trade-off discussion.
  • Activity log redaction. ~/.thlibo/logs/*.ndjson records byte-count telemetry only; subprocess stderr and error strings pass through a secret-pattern redactor before disk write (AWS keys, GitHub PATs, HuggingFace tokens, generic SECRET= / API_KEY= assignments). The redactor is a best-effort backstop, not a replacement for keeping secrets out of subprocess output.
  • Telemetry is opt-in and content-free. OpenTelemetry emission (see Monitoring) is off unless THLIBO_ENABLE_TELEMETRY is set. When on, it emits only sizes, counts, durations, and fixed enum labels to a collector you configure — never tool output, prompts, commands, or file paths; user processor names redact to "custom". It fails open (a dead collector never blocks a tool call). See THREAT_MODEL.md (2026-07-14 addendum) and ADR 0011.
  • Inferd version gate. thlibo refuses to delegate to inferd binaries older than MinInferdVersion (currently v0.4.0 — the first release with the unified IPC wire thlibo's codec speaks; earlier daemons are unreachable). The gate detects an older daemon and triggers a fresh inferd install instead of failing open forever.
  • Supply chain. Every GitHub Action in this repo is pinned by commit SHA. Every release archive, the SHA256SUMS, and the CycloneDX SBOM are signed with cosign via Sigstore's keyless flow — no key to manage, identity rooted in the GitHub OIDC token for .github/workflows/release.yml at the release tag, transparency- log entry published to rekor.sigstore.dev. Verification command is in the release notes for each tag. The release pipeline runs the installer scripts against the just-built archive on ubuntu-latest + windows-latest before gh release create — a broken installer cannot ship. A full threat model lives at THREAT_MODEL.md.

Known limitations

  • Bash, PowerShell, Read, and Write/Edit tool coverage; MCP tools bypass. The PreToolUse hooks intercept Claude Code's Bash tool, PowerShell tool (when CLAUDE_CODE_USE_POWERSHELL_TOOL=1), Read tool (for files dragged into the window or referenced by path), and Write / Edit tools (when shorthand auto-apply is enabled). Grep / Glob / MCP-served tools bypass the hook — their inputs and outputs are not intercepted.
  • Cursor: shell + file reads, no MCP. thlibo install --cursor installs two preToolUse hooks. The Shell hook rewrites the command (via updated_input) to run through thlibo exec, so terminal output is compressed before the model reads it. The Read hook rewrites tool_input.file_path to a pre-built thlibo case (compressed.log) so large logs/PDFs are compressed too — bounded by a timeout (THLIBO_READ_TIMEOUT, default 20s) so a slow scanned-PDF OCR falls through to the original rather than hanging Cursor. Cursor's hooks still cannot substitute MCP-tool output for built-in tools (afterShellExecution is observe-only; updated_mcp_tool_output is MCP-server-only). User-level ~/.cursor/hooks.json loads automatically; project-scoped hooks require a trusted workspace.
  • Compound shell commands pass through. git status | head or cmd1 && cmd2 are not rewritten — only single-program invocations. thlibo rewrite matches on argv[0] and deliberately doesn't try to parse a shell AST.
  • Inferd is a separate dependency. thlibo no longer ships its own inference daemon. The first thlibo install on a fresh host pulls inferd over HTTPS and runs inferd's installer; if you need air-gapped install, fetch inferd manually first (see github.com/3rg0n/inferd) and thlibo's probe-then-delegate will use it without touching the network.

Development

  • AI-assistant guidance: CLAUDE.md.
  • Architecture decisions: docs/adr/.
  • Changelog: CHANGELOG.md.
  • Run the tests: go test ./... -timeout 120s
  • Scanner sweep: go vet ./... && staticcheck ./... && gosec ./... && govulncheck ./...
  • Fuzz the PDF parser (the untrusted-input boundary): go test ./internal/pdf/ -run '^$' -fuzz '^FuzzOpenBytes$' -fuzztime 60s. Three targets — FuzzOpenBytes, FuzzParseInlineDict, FuzzDecodeTextString — run nightly in CI, not per-PR. Touching the parser? Soak it before the PR rather than waiting for the nightly.
  • Benchmark the two filters whose cost is load-bearing: go test ./internal/processors/ -run '^$' -bench . -benchmem. Compare revisions with benchstat rather than eyeballing one run. Two ceiling tests run with the ordinary suite (-run StaysUnderBudget) — for cordon-filter that ceiling is an availability guard, not a speed one: its k-NN pass is O(n²), and once it can no longer finish inside CORDON_TIMEOUT the filter fails open and silently becomes a no-op that every other test still passes.

Project layout

cmd/
  thlibo/             User CLI: rewrite, exec, compress, case, install,
                      uninstall, shorthand, version.
internal/
  adapters/
    claudecode/       PreToolUse hooks (Bash + PowerShell + Read + Write/Edit),
                      /caselog skill, settings.json merger.
    codex/            PostToolUse hook (decision: block) + config.toml merger.
    cursor/           preToolUse hooks (Shell + Read updated_input) + hooks.json merger.
    copilot/          preToolUse (modifiedArgs) + postToolUse (modifiedResult) hooks.
  casefile/           `thlibo case` directory builder (compressed.log + summary + meta).
  config/             ~/.thlibo/config.yaml read/write.
  execpolicy/         `thlibo exec` allow/deny policy.
  inferd/             thlibo's own codec for inferd's v2 IPC wire
                      (length-prefixed framing); no client dependency.
  install/            Disk mirror + per-platform inferd sidecar installer
                      (probe-then-delegate) + v0.5 → v0.6 migration.
  logx/               NDJSON activity log with rolling rotation + secret redactor.
  middleware/         Main flow: short-circuit → fast-path → router → chain.
  processors/         Registry, descriptors, script+prompt dispatch, thought-stripping.
  promptsan/          Gemma marker sanitiser for untrusted tool output.
  router/             Processor routing via inferd response_format (JSON-Schema).
  shellcmd/           Minimal shell-command argv[0] extractor.
  shorthand/          LLM-facing prose compression (SKILL.md / CLAUDE.md).
  update/             Background release check + upgrade banner.
  version/            Build-tag constant (overridable via -ldflags).
processors/           Embedded built-ins (go:embed).
skills/               Claude Code skills: /caselog.

Why "thlibo"?

The Greek word θλίβω means to press, squeeze, compress. Same root as "tribulation" — being crushed down. Thlibo crushes tool output before the model ever sees it.

About

AI tool output compressor for Claude Code + Codex CLI. Local Gemma 4 for unknown output; deterministic Python filters for git/npm/cargo.

Resources

Contributing

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages