Skip to content

Latest commit

 

History

History
192 lines (142 loc) · 8.91 KB

README.md

File metadata and controls

192 lines (142 loc) · 8.91 KB

shunt

A Claude Code plugin that shunts I/O-heavy work to AiKA modes, saving 82-94% of tokens on large file reads and boilerplate generation.

How it works

Three layers, from hard gate to soft suggestion:

  1. Hooks block Claude from reading large files and redirect to the bulk-reader skill
  2. Scripts handle the AiKA invocation and output cleanup
  3. Skills tell Claude when and how to call the scripts

Claude never assembles bash pipelines from prose. It calls a script with named arguments. The scripts handle everything internally.

Delegation goes through the Portal CLI actions registry — one aika:invoke-chat call per delegation — so the plugin works against any Portal instance with AiKA enabled. Modes are addressed by name and resolved server-side: case-insensitive, preferring your own mode, then your groups', then public ones; a name matching nothing or several modes equally fails with the candidate ids.

Prerequisites

  • jq — brew install jq
  • The portal plugin from this marketplace, which provides the Portal CLI that shunt delegates through:
claude plugin install portal@portal

Then, in a new session, set up and authenticate the CLI against your Portal instance:

/portal:setup

Check whether the two AiKA modes (bulk-reader and code-writer) already exist on your instance — many instances ship them as public modes:

portal-cli actions aika:list-modes --json --input '{"search": "bulk-reader"}'

If they exist, no mode creation is needed — just install the plugin and go. If not, or to create your own customized versions (e.g. different model or instructions):

portal-cli actions aika:create-mode --input '{
  "name": "bulk-reader",
  "description": "Bulk file reader for code analysis",
  "instructions": "You are a precise code analyst. Read the provided files and answer the question concisely. Output structured bullets only. No greetings, no prose, no preambles, no summaries. Lead every bullet with the exact name, type, or line number. Use nested bullets for details. Skip anything the caller did not ask for.",
  "tags": ["coding", "delegation"],
  "resource_limits": { "temperature": 0.2 }
}'

portal-cli actions aika:create-mode --input '{
  "name": "code-writer",
  "description": "Boilerplate code generator",
  "instructions": "You generate code files based on a spec and reference files. Match the existing patterns, conventions, naming, and style exactly. Output only the code — no explanations, no markdown fences unless asked. If the spec is ambiguous, make reasonable choices that match the patterns in the reference code.",
  "tags": ["coding", "delegation"],
  "resource_limits": { "temperature": 0.2 }
}'

A mode you create is private and owned by you, and name resolution prefers your own modes — so your customized bulk-reader automatically shadows the public one, no configuration needed.

Plugin structure

shunt/
├── .claude-plugin/
│   └── plugin.json          # Plugin manifest (name, description, version)
├── hooks/
│   ├── hooks.json           # Hook registration (PreToolUse matchers)
│   ├── check-file-size      # Blocks Read on files > 350 lines
│   └── check-bash-read      # Blocks cat/head/tail on large files
├── scripts/
│   ├── lib/
│   │   └── aika.sh          # Shared aika:invoke-chat plumbing
│   ├── bulk-read            # Invokes the bulk-reader mode
│   └── code-write           # Invokes the code-writer mode
├── skills/
│   ├── bulk-reader/
│   │   └── SKILL.md         # When/how to call bulk-read
│   └── code-writer/
│       └── SKILL.md         # When/how to call code-write
└── evals/
    ├── run.sh                # Runs hook + transport evals (51 tests)
    ├── hook-evals.json       # Read hook test cases (17)
    ├── bash-hook-evals.json  # Bash hook test cases (17)
    ├── transport-evals.sh    # scripts/lib/aika.sh against a stubbed CLI (17)
    ├── evals.json            # End-to-end skill test cases (3)
    ├── benchmarks.json       # Token savings scenarios (4)
    └── fixtures/             # Test fixture files

Scripts

bulk-read

Delegates file reading to AiKA. Files are wrapped in XML tags (<file path="...">) for clear boundaries.

bulk-read --question "What does this service do?" --paths src/Service.java src/Handler.java

# Follow-up: ask again with the same paths
bulk-read --question "Which methods call the database?" --paths src/Service.java src/Handler.java

code-write

Delegates boilerplate generation to AiKA. Strips markdown fences from output. Can write directly to disk via --target. --reference is required — without a file to match patterns against, the worker would generate context-free code that fits nothing in the project.

# Generate and write to file
code-write --spec "Write tests for UserService" --reference tests/OrderTest.java --target tests/UserTest.java

# Build on what was just generated by referencing it
code-write --spec "Now add edge case tests" --reference tests/UserTest.java --target tests/UserEdgeCases.java

# Output to stdout
code-write --spec "Generate a config stub" --reference config/existing.yaml

One shot per call

aika:invoke-chat is ephemeral: nothing is stored server-side, and the action's own follow-up mechanism is for the caller to replay prior turns. Replaying a file corpus is the exact cost this plugin exists to avoid, so shunt does not do it — every call stands alone. Re-sending files is free where it matters, because the corpus goes to the worker model and never enters Claude's context.

Hooks

check-file-size (Read hook)

Fires on every Read tool call. Blocks full-file reads on files exceeding MIN_LINES (default: 350, configurable via SHUNT_MIN_LINES env var). Allows through:

  • Targeted reads (offset or limit set)
  • Files under the threshold
  • Nonexistent files (let Read handle the error)

check-bash-read (Bash hook)

Fires on every Bash tool call. Catches cat, head, tail, less, more on large files. Allows through:

  • Piped commands (cat file | grep) — targeted reads
  • Redirections (cat file > out) — not reading into context
  • Commands with flags that indicate targeted reads
  • Non-read commands (git status, grep, etc.)

Configuration

All settings are environment variables — add them to the env block in .claude/settings.json.

Variable Default Purpose
SHUNT_MIN_LINES 350 Line count above which the Read hook blocks and redirects
SHUNT_PORTAL_INSTANCE CLI default Portal instance name or URL to invoke against
PORTAL_CLI_BIN portal-cli, else npx Override how portal-cli is launched
SHUNT_MAX_PAYLOAD_BYTES 400000 (120000 on Linux) Request ceiling, since input travels through argv
SHUNT_TIMEOUT_SECONDS 180 Timeout for one action invocation
SHUNT_BULK_READER_MODE_ID — Pin a specific mode id if the name is ambiguous
SHUNT_CODE_WRITER_MODE_ID — Pin a specific mode id if the name is ambiguous

What doesn't get delegated

The plugin is designed to know when NOT to delegate:

  • Debugging — requires Claude's reasoning, not a summary
  • Editing — Claude needs exact content in context; use targeted reads (offset/limit)
  • Small files — delegation overhead exceeds savings under 350 lines
  • Architectural decisions — judgment calls stay on Claude

Evals

# Hook routing + transport plumbing — needs no Portal access
bash evals/run.sh

# Also re-measure token savings against the real modes — needs portal-cli auth
bash evals/run.sh --benchmark

Benchmarks

Tested against a 162K-line Java monorepo:

Scenario Lines Without shunt With shunt Savings
Single large file 4,014 33,684 tokens 5,737 tokens 82%
Source + test pair 7,408 75,990 tokens 4,148 tokens 94%
Multi-file cross-service 1,281 16,221 tokens 821 tokens 94%
Code-write 3,667 40,614 tokens + generation 833 lines to disk -

Mean bulk-read savings: 90%

Known limitations

  • No enforcement for code-writer — only bulk-reader has hook enforcement. Code-writer relies on Claude recognizing when to use it via the skill description.
  • Request size — aika:invoke-chat input is passed on the command line, so a request must fit in ARG_MAX (1 MB on macOS, shared with the environment; Linux additionally caps a single argument at 128 KiB). shunt refuses anything over SHUNT_MAX_PAYLOAD_BYTES with a clear error rather than failing with E2BIG. Split into smaller batches.
  • Invocation timeout — shunt caps one action invocation at SHUNT_TIMEOUT_SECONDS (default 180). Very large generations can exceed it; raise the timeout or split the spec into smaller calls.