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.
Three layers, from hard gate to soft suggestion:
- Hooks block Claude from reading large files and redirect to the bulk-reader skill
- Scripts handle the AiKA invocation and output cleanup
- 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.
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
shunt sets PORTAL_CLI_ENABLE_EXPERIMENTAL for its own calls (the actions registry is experimental in portal-cli 0.4.x); you only need it exported for the manual portal-cli commands below.
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.
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 (50 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 (16)
โโโ evals.json # End-to-end skill test cases (3)
โโโ benchmarks.json # Token savings scenarios (4)
โโโ fixtures/ # Test fixture files
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
bulk-read --question "Which methods call the database?" --paths src/Service.java src/Handler.java
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.
code-write --spec "Write tests for UserService" --reference tests/OrderTest.java --target tests/UserTest.java
code-write --spec "Now add edge case tests" --reference tests/UserTest.java --target tests/UserEdgeCases.java
code-write --spec "Generate a config stub" --reference config/existing.yaml
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.
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)
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.)
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 , elsenpx |
Override how portal-cli is launched |
SHUNT_MAX_PAYLOAD_BYTES |
400000 (120000 on Linux) |
Request ceiling, since input travels through argv |
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 |
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
bash evals/run.sh
bash evals/run.sh --benchmark
Tested against a 162K-line Java monorepo:
| Scenario | Lines | Without shunt | With shunt | Savings |
|---|---|---|---|---|
| Single large file (SpotifyUri.java) | 4,014 | 33,684 tokens | 5,737 tokens | 82% |
| Source + test pair (PromotionRuleRepository) | 7,408 | 75,990 tokens | 4,148 tokens | 94% |
| Multi-file cross-service (permission handlers) | 1,281 | 16,221 tokens | 821 tokens | 94% |
| Code-write (generate tests from reference) | 3,667 | 40,614 tokens + generation | 833 lines to disk | - |
Mean bulk-read savings: 90%
- 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-chatinput is passed on the command line, so a request must fit inARG_MAX(1 MB on macOS, shared with the environment; Linux additionally caps a single argument at 128 KiB). shunt refuses anything overSHUNT_MAX_PAYLOAD_BYTESwith a clear error rather than failing withE2BIG. Split into smaller batches. - 30-second invocation cap โ portal-cli aborts an action invocation after 30s and does not expose a timeout flag. Large generations can exceed it; split the spec into smaller calls.