Skip to main content

Chain of shell-command shims routing calls through semantic tools, compression backends, and real binaries

Project description

posa

Routes AI-agent tool usage through more efficient means through semantic MCP tools, CLI output compression, and hook blocks.

All agent calls are handled to choose the most effective ones, using fallbacks where it makes sense, and hard blocking where default tools and commands don't work as well.

One installer wires up three layers covering the three paths an agent can take when interacting with your code.

posa, shorthand for composable, is a tool composes multiple interceptors (semantic backends, compression wrappers, real binaries) into one chain, and each link is independently pluggable via mappings.json. It's also an acronym: Proxy Overlay for Shell-semantic Agent-actions — a routing layer on agent shell and tool calls

The three layers

  1. PATH shims — intercept shell commands (grep, cat, sed, find, etc.) when an agent runs them via a Bash/shell tool. Blocks with a message if a semantic alternative exists; otherwise silently routes through the compression wrapper for token savings.
  2. PreToolUse hook — intercepts Claude Code's built-in tools (Grep, Read, Edit, Write, Glob), which have their search/edit engines compiled into the claude binary and don't go through PATH.
  3. Compression fallback — when a wrapper backend (e.g. RTK) is installed, the shim and the escape hatch (grep_, cat_, etc.) both route through it instead of running the raw binary.

Why posa

posa enables a more effective, hard denial by knowing both backends are present. Blocks and surfaced reminders are designed around how agents actually behave, not how they ideally should.

Alone, token-reduction and context-quality tools for AI coding agents fall into two camps with the following benefits:

  • Transparent CLI proxies like RTK compress noisy command output (git, docker, kubectl, grep, etc.) so it takes fewer tokens.
  • Semantic MCP servers like Serena, jCodeMunch, tree-sitter-mcp, and Code Pathfinder use LSP or AST to give agents symbol-level understanding instead of raw text.

However, neither cleanly covers the other's ground:

  • Semantic MCP alone like Serena misses Bash-tool commands that bypass its MCP layer (grep, cat, sed via shell), does no output compression, and even with its hook installed, only issues a soft non-blocking remind — not a hard deny. That's not an oversight: without knowing a compression fallback exists, hard denial would leave the agent with no viable raw path. Serena's hook can't know RTK is present.
  • CLI proxy alone like RTK has no semantic alternative and no hook coverage for built-in Grep/Read/Edit.

Claude Code's system prompt instructs agents to prefer built-in tools, which means current token optimization solutions are structurally side-stepped. Savings are triggered less often.

posa's cross-layer messaging enforces every effective denial, while also surfac all relevant alternatives for the agent to utilize. Serena's provided hooks stay at a soft remind — not an oversight, but the only safe choice when it can't know a compressed escape hatch exists. RTK can't reach built-in tools at all. posa knows both are present, so it can hard-deny Grep and tell the agent exactly what to do next: "use find_symbol, or if you truly need raw grep, Bash grep_ still routes through rtk for compression."

Manually stacking Serena and RTK doesn't produce this — neither knows the other exists. One install command with posa wires up all three layers.

Intercepted agent calls coverage matrix

Scenario Semantic MCP alone (e.g. Serena) CLI proxy alone (e.g. RTK) posa
Bash grep / cat / find 🟡 optional hook or manual wiring. soft, non-blocking remind only ✅ compresses, no semantic nudge ✅ tool aware block + grep_ bypass hint[¹]
Built-in Grep / Read / Edit / Write / Glob 🟡 same — rate-limited, can always be bypassed, not installed by default ❌ rtk hooks Bash only, can't reach built-ins ✅ deny surfaces both semantic alt and compressed Bash <tool>_[¹]
Escape hatch grep_ / cat_ / … ❌ concept doesn't exist ❌ concept doesn't exist ✅ reminder + compressed if rtk present
Interactive terminal ($ grep …) ❌[²] ❌[²]

[¹] Exact behavior (always-deny vs rate-limit) is per-tool, depending on which backends are available — see Routing table. Also, file extension aware to minimize agent noise for files that don't benefit much from semantic tools and compression

[²] Interactive shells don't go through an AI-tool PATH. Use rtk grep directly or a shell alias; no AI-layer tool addresses this.

Routing table

Every intercepted call gets routed based on which backends are available for that specific tool. The shim and hook apply the same rule — block only when blocking is free, i.e. the agent has a viable alternative for both semantic and raw use.

Semantic tool available Compression wrapper available Routing Example tools
Always deny — message names both the semantic alternative and the compressed Bash <tool>_ escape hatch. Blocking is safe here because the agent has a better path and a compressed raw fallback — the condition Serena's hook alone can never guarantee, which is why it stays at a soft remind. Bash grep, cat, find → serena + rtk. Built-in Grep, Read, Glob.
Rate-limit (3 consecutive / 4 combined, 2-min cooldown). Escape hatch still works but loses compression, so tolerate some raw use. Bash sed. Built-in Edit, Write. Hook-only WebSearch, WebFetch.
Allow transparently — shim wraps the call through rtk automatically, no message. Bash git, ls, docker, kubectl, aws, curl, jq, tree, diff, plus any rtk-discovered command on your machine.
Pass through — posa has nothing to do; call hits the real binary. ssh, tar, make, or any tool not in mappings.json and not covered by rtk.

When the hook blocks, the denial message always names the semantic alternative and (when wrap-ready) the compressed Bash <tool>_ escape hatch.

File extension aware routing behavior

Availability-based routing (the table above) is the base policy. Read/Write/Edit adds a second dimension: the file extension, because semantic symbol tools don't help on a README or a log file regardless of which backend you have running. The strict policy would be a pure false positive there.

File extension Tier Behavior for Read / Edit / Write
.md, .txt, .env, .rst Pass through Hook doesn't intercept. Small non-code files where semantic tools don't help and compression wouldn't buy much.
.json, .toml, .yaml, .yml, .xml, .html, .css, .ini, .cfg, .conf Lax Rate-limit at lax_threshold (default 8) with a gentler message pointing at the relevant escape hatch (cat_ / sed_). Coverage depends on your backend — jcodemunch/tree-sitter-mcp cover some of these via tree-sitter; Serena's default LSP set doesn't.
.log, .csv, .diff, .patch, .lock Strict (intentional) Non-code but often large. Kept strict so the deny message nudges the agent toward cat_ (rtk-compressed) instead of letting built-in Read dump a raw multi-MB file into context.
Everything else (code, unknown, no extension) Strict Uses the base routing-table policy above. The strict message qualifies semantic alternatives with "on code files" so it reads cleanly for non-code too.

Both lists live in mappings.json under hooks.pass_through_extensions and hooks.lax_extensions and are meant to be edited — move an extension between buckets or add a new one to match your backend stack. See mappings.json structure for the full hooks config.

Not extended to Grep / Glob: their inputs (pattern, optional include/path) are fuzzier — an unanchored grep can hit any file type, and a glob like **/*.{py,md} straddles categories. Extending extension-awareness there would need input parsing and risks lawyer-gaming ("just grep with include=*.md to bypass the block"). Left strict for now; open to revisiting if false-positives show up in practice.

mappings.json is yours to tune

Beyond extension lists, the whole config file is user-editable: rate-limit thresholds, workflows, which backends to treat as recommended, semantic alternatives for each shim, hook redirects for new built-in tools — all in one file. Run posa install again after any change to regenerate the shim dir and hook script.

Customization is only really practical on a development install today. For pipx/uv-tool installs the file lives deep inside the tool's venv (posa config path prints its location) and any upgrade will overwrite your edits. If you want to tune backends or thresholds, use the editable install flow in the Development section — it's a ~2-minute setup.

A proper user-writable config overlay (~/.posa/mappings.json shadowing the bundled defaults, surviving upgrades) is on the roadmap so pipx users get first-class customization too; for now, dev install is the supported path.

See mappings.json structure for the schema.

Install

Requires Python 3.9+. (For running from a git checkout without installing, see the Development section below.)

pipx install posa        # or: uv tool install posa
posa install             # interactive: one question for defaults, "custom" for picking
posa install -y          # non-interactive, auto-install missing recommended backends
posa install --backend serena    # force-enable specific backend(s)
posa install --project           # write to .claude/settings.json in cwd instead of ~
posa install --no-hooks          # skip the PreToolUse hook
posa install --isolate           # separate shim copy per provider
posa list                        # show active backends, shims, hook redirects, drift warnings
posa doctor                      # audit provider configs for stale shim-dir PATH entries
posa doctor --fix                # rewrite env.PATH in provider configs to point at the real shim dir
posa config path                 # print the active mappings.json path (useful for customization)

Installed shims live in ~/.posa/shims/ (materialized from the package on first install). The interactive flow shows what's recommended, what's optional, what each backend does and why, then asks one question. Pick custom to choose backends individually.

The entire config — backend choices, rate-limit thresholds, per-extension hook behavior, workflows — lives in mappings.json inside the installed package and is meant to be edited. Re-run posa install after any change.

rtk auto-discovery

When rtk is detected, posa install enumerates rtk's supported commands via rtk --help and confirms each one with rtk's own oracle rtk rewrite <cmd> (documented as rtk's "single source of truth for hooks"). Any rtk-supported command whose real binary is also on PATH — and isn't already hand-curated in mappings.json — gets a compression-only shim auto-generated at install time. This tracks whatever rtk version is installed without hand-maintaining a static command list. Example auto-discoverable tools: git, ls, docker, kubectl, aws, curl, cargo, npm, pnpm, pytest, tsc, gh, ruff, … (rtk ships wrappers for ~60 commands; on any machine you get whatever is actually installed).

Drift detection

posa list and posa doctor detect a common failure mode: provider settings.json files contain an absolute-path env.PATH baked at install time. If ~/.posa/ moves or a stale project-scope config points to a shim dir that no longer exists (or is empty), tools silently stop being shimmed. posa doctor --fix rewrites stale entries to the current SHIM_DIR/gen.

Uninstall

posa uninstall                   # remove shims + hooks + PATH from configs, delete ~/.posa/
posa uninstall --project         # remove project-level install
posa uninstall --clean-backends  # also uninstall MCP backend binaries (serena, etc.)

Customization

mappings.json has the following that you can edit for your preferred workflow:

  • default (serena + rtk) — best balance for most users; covers editing + CLI compression
  • minimal (serena) — just code semantics, no compression layer
  • max_token_savings_readonly (jcodemunch + rtk) — read-heavy work, no editing; biggest token wins on reads
  • call_graph_focused (codepathfinder + rtk) — tracing "who calls X" across the codebase
  • polyglot_codebase (tree-sitter-mcp + rtk) — mixed-language repos where one LSP won't cover everything
  • with_web (+exa +firecrawl) — additive layer when the agent needs web search / scraping

Backend catalog

Three categories. Stack one from each freely; pick at most one code-semantic backend (they overlap on the same job).

Code-semantic (pick one)

Backend Default What it does Pick it when
serena LSP-powered find/read/edit symbols You need editing + find_references. The only one that covers edits via MCP.
jcodemunch Tree-sitter symbol indexing, 95%+ token reduction (read-only) Read-heavy exploration. Biggest token wins, but can't edit.
codepathfinder AST call graph, "who calls X" Tracing relationships across a codebase. Answers call-graph questions directly instead of string-matching.
tree-sitter-mcp AST queries across 25+ languages Polyglot repos where one LSP can't cover everything.

CLI output

Backend Default What it does
rtk Compresses CLI output 60-90% (git, docker, kubectl, grep, cat, …). The only option here today.

Web

Backend What it does Pick it when
exa Real code/web search (GitHub, docs) Agent needs to search for code snippets, documentation, or Stack Overflow answers.
firecrawl JS-rendered scraping → markdown Agent needs to read websites that require JS to render (SPA docs, dashboards).

Things worth knowing

  • settings.json env.PATH can't reference $PATH. The installer resolves the full path at install time.
  • Running rtk init -g in addition to posa is redundant for Claude Code coverage. Both install a Claude Code PreToolUse hook that covers Bash tool calls — rtk's hook redirects to rtk grep directly, ours routes through rtk grep via wrap_cmd only when the semantic backend isn't ready. If you install both, rtk's hook fires first and redirects Bash(grep)rtk grep before our PATH shim sees it, so our Serena nudge for Bash-tool grep is bypassed (you still get the nudge for built-in Grep via our separate hook). rtk init -g does NOT install shell aliases, so it doesn't help with interactive terminal use either way. Install ordering doesn't matter — our hook no-ops for Bash calls (it only counts built-in tool names), so the two hooks don't interfere in the PreToolUse array.
  • mappings.json uses hardcoded tool names (e.g., find_symbol). If an upstream MCP backend renames one of its tools, the use_instead hint becomes stale until you update the mapping.
  • Non-Claude-Code users (Copilot, Cursor, Windsurf, etc.) get the PATH-shim layer only — same coverage as stacking Serena + RTK manually, just with a unified installer. No provider currently exposes a hook API comparable to Claude Code's, so the built-in-tool coverage gap is an ecosystem constraint that affects every alternative equally.

How the layers interact at runtime

agent calls `grep "x" file.py` via Bash tool
        ↓
    PATH shim fires (gen/grep)
        ↓
    Serena ready (readiness_cmd succeeds)?
    ├─ YES → block, stderr: "use find_symbol instead"
    │         (adds "bypass preserving compression: grep_" if rtk available)
    └─ NO  → RTK installed?
             ├─ YES → exec `rtk grep "x" file.py` (compressed output)
             └─ NO  → exec real `grep "x" file.py` (raw output)

agent calls `grep_ "x" file.py` (escape hatch — explicit bypass)
        ↓
    stderr: "[ESCAPE HATCH] consider find_symbol"
        ↓
    RTK installed? → use it; else real grep

agent uses Claude Code's built-in `Grep` tool
        ↓
    PreToolUse hook fires
        ↓
    tool_name in TOOL_MAP?
    ├─ NO  → no-op (Bash calls, unmapped tools fall through here)
    └─ YES → wrap-ready for this tool?
             ├─ YES → deny, surface both semantic alt + `Bash <tool>_`
             └─ NO  → count consecutive calls
                      ↓
                      Threshold reached (3 same / 4 mixed)?
                      ├─ YES → deny with suggestion, reset, 2-min cooldown
                      └─ NO  → allow, increment
             ↓
             Counter resets when agent uses any mcp__<backend>__* tool

Architecture

posa/
  mappings.json         # human-editable: backends, shims, hook_redirects, workflows
  install.py            # generates everything, configures provider, installs hooks
  uninstall.py          # reverses install.py, optionally removes backend binaries
  cli.py                # entry point: posa {install|uninstall|list|doctor}
  shims/
    base.sh             # shared logic: tool-name resolution, real-binary lookup, mappings load
    tool_shim.sh        # blocking shim with compression fallback
    escape_hatch.sh     # always-pass-through with compression preference
~/.posa/shims/          # materialized at install time (user-writable)
  gen/                  # regenerated on every install
    tool_mappings.sh    # 6-column pipe-delimited, generated from mappings.json
    pretooluse_hook.py  # generated, self-contained, stateful via JSON file
    grep -> ../tool_shim.sh
    grep_ -> ../escape_hatch.sh
    ...
test/                   # pytest suite

Shim scripts use only bash builtins internally — they never call grep/sed/cat/etc., which would recurse into themselves.

mappings.json structure

Top-level keys:

  • backends — MCP servers (or shell wrappers like RTK). Each defines install_check, install_cmd, start_cmd/readiness_cmd (for MCP servers) or wrap_cmd (for shell wrappers), recommended, category, description, benefits, url.
  • shims — bash commands that get a PATH shim. Each can have a primary backend (semantic alternative) and/or compression_backend (fallback wrapper). hook_tools lists Claude Code built-in tools that should also nudge to the same alternative.
  • hook_redirects — hook-only mappings for tools without a shell command (e.g., WebSearchexa).
  • hooks — PreToolUse hook config:
    • consecutive_threshold, combined_threshold, cooldown_seconds — thresholds for the rate-limit policy (see Routing table).
    • lax_threshold — rate-limit ceiling for the lax tier below.
    • pass_through_extensions — file extensions where Read/Edit/Write skip the hook entirely. Reserved for small non-code files where semantic tools don't help and compression wouldn't buy much (default: .md, .txt, .env, .rst). Large-prone non-code like .log is deliberately not here — see Routing table.
    • lax_extensions — file extensions where Read/Edit/Write rate-limit at lax_threshold with a gentler warning (e.g. .json, .yaml). Coverage depends on the active semantic backend — edit this list to match your stack (remove an extension to promote it to strict, move to pass_through_extensions to silence it entirely).
  • workflows — recommended backend combinations for different use cases (rendered in "Pick your backend combo" above).

Example shim with both backends:

"grep": {
  "use_instead": ["find_symbol", "find_referencing_symbols"],
  "backend": "serena",            // primary: blocks when ready
  "compression_backend": "rtk",   // fallback: routes through rtk if not blocking
  "hook_tools": ["Grep"]          // also nudge built-in Grep tool via hook
}

Adding things to mappings.json

New shim:

"shims": {
  "awk": {
    "use_instead": ["get_symbols_in_file"],
    "backend": "serena",
    "compression_backend": "rtk",
    "hook_tools": []
  }
}

New backend (MCP server):

"backends": {
  "my-lsp": {
    "description": "...",
    "category": "code-semantic",
    "recommended": false,
    "install_check": "command -v my-lsp",
    "install_cmd": "pip install my-lsp",
    "uninstall_cmd": "pip uninstall -y my-lsp",
    "start_cmd": "my-lsp serve",
    "readiness_cmd": "pgrep -f my-lsp",
    "tool_prefix": "mcp__my_lsp__",
    "url": "https://..."
  }
}

New backend (CLI wrapper like RTK):

"backends": {
  "compresscli": {
    "install_check": "command -v compresscli",
    "wrap_cmd": "compresscli"
  }
}

Hook-only redirect (no shell command involved):

"hook_redirects": {
  "WebSearch": { "use_instead": ["web_search"], "backend": "exa" }
}

Re-run posa install after any change.

Adding a new AI provider

Add to PROVIDER_CONFIGS in both posa/install.py and posa/uninstall.py:

PROVIDER_CONFIGS = {
    ...
    "newprovider": (".newprovider", "newprovider.json"),
}

Development

git clone <repo> && cd posa
pip install -e ".[dev]"       # installs posa + pytest
python -m pytest test/ -v

The [dev] extra pulls in pytest. Without it, python -m pytest won't find the test runner unless you have pytest installed globally.

Running posa from a checkout

Three ways, pick what you need:

# 1. Editable install — gives you the real `posa` entry point, same as end users.
pip install -e ".[dev]"       # or `pip install -e .` if you don't need tests
posa install
posa uninstall

# 2. Module invocation — no install required, runs the CLI directly.
python3 -m posa.cli install
python3 -m posa.cli uninstall

# 3. Direct script — skips cli.py dispatcher, runs install/uninstall as top-level scripts.
python3 posa/install.py
python3 posa/uninstall.py

All three share the same code path and state (~/.posa/, provider configs). Pick #1 if you want the CLI experience end users will have. Pick #2 or #3 for quick iteration without reinstalling after every change.

Customizing mappings.json

A dev-install is currently the supported way to customize posa. Workflow:

pip install -e .                   # if you haven't already
$EDITOR $(posa config path)        # or: $EDITOR posa/mappings.json
posa install                       # regenerates shim dir + hook script

What's commonly tuned:

  • Swap the code-semantic backend — change shims.<tool>.backend from serena to jcodemunch / tree-sitter-mcp / codepathfinder (or add them under backends if they're not listed). Pick one per code-semantic slot; they overlap.
  • Rate-limit knobshooks.consecutive_threshold, hooks.combined_threshold, hooks.cooldown_seconds, hooks.lax_threshold.
  • Extension tiershooks.pass_through_extensions and hooks.lax_extensions (see Context-aware Read tiers for the semantics).
  • New shims or hook redirects — see Adding things to mappings.json.

The ~/.posa/mappings.json user-overlay that would let pipx/uv-tool users customize without a dev install is on the roadmap; not shipped yet.

Test suite

The pytest suite covers install/uninstall in shared/isolated/project modes, mappings validation, hook script behavior (per-tool always-deny vs rate-limit, cooldown, deny semantics, backend prefix detection), shim blocking/fallthrough, escape hatch behavior (including wrapper-passthrough PATH-sanitization regression), compression layering, missing binaries, PATH recursion avoidance, stdin passthrough, argument edge cases, rtk auto-discovery, posa list output, and posa doctor drift audit/fix.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

posa-0.1.0.tar.gz (54.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

posa-0.1.0-py3-none-any.whl (31.6 kB view details)

Uploaded Python 3

File details

Details for the file posa-0.1.0.tar.gz.

File metadata

  • Download URL: posa-0.1.0.tar.gz
  • Upload date:
  • Size: 54.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.4

File hashes

Hashes for posa-0.1.0.tar.gz
Algorithm Hash digest
SHA256 d4f0aedf4539654d453e79ceddeb47c03baba10130c117868ea754ec981240c2
MD5 8615bb97853ac9f929a5c8f77ffbf047
BLAKE2b-256 46fd92161f688e7ef4eae6c7147039b9905aa3cf17c4e982a257ecbfb0a47901

See more details on using hashes here.

File details

Details for the file posa-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: posa-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 31.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.4

File hashes

Hashes for posa-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 da0dbe62209579fc3b0d38a0863a4475e595fd06a9ce5b588c0df2b75553e8a9
MD5 1005e7de1610c467e509c2af2d5b8346
BLAKE2b-256 9e9fd2d0b62df303a60fb19c301cc0f1204ce71641e51ad3c81184708de0bb2d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page