isthmos

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 21, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

isthmos

Build Status Latest Release OpenSSF Scorecard License

isthmos (Greek: ισθμός), the narrow passage your tool outputs squeeze through.

A local context-compression layer for agent tool outputs. The core is agent-agnostic: JSON field pruning driven by per-tool rules, importable as a Go package. Adapters connect it to whatever runs your LLM: a native Claude Code PostToolUse hook that rewrites tool_output via updatedToolOutput, and a generic filter mode that works with any agent or CLI that can pipe through a command. Nothing leaves your machine and nothing sits in the credential path.

Status

Early but working: rule-based JSON field pruning plus byte-level measurement.

Why

Agent context spend is dominated by tool outputs (verbose MCP JSON, log dumps, API payloads), yet most token hygiene tooling only targets the input/prefix side. isthmos targets the output side, at the only point where the payload is visible before the model reads it.

Usage

Claude Code (native hook)
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "mcp__.*",
        "hooks": [
          {"type": "command", "command": "$HOME/.local/bin/isthmos hook", "timeout": 5}
        ]
      }
    ]
  }
}
Any other agent (generic filter)
some-tool --json | isthmos filter -tool mcp__github__search_repos

Stdin in, pruned stdout out. Wire it into any wrapper, shell function, or orchestrator that can interpose a pipe. Non-JSON payloads pass through untouched.

Checking your setup
$ isthmos doctor
version: 0.1.0
rules:   /Users/you/.config/isthmos/rules.json: ok, 4 rules
store:   ok, 12 entries
measure: /Users/you/.local/state/isthmos/measure.jsonl: 84.2KB, last write 2026-07-21T09:14:02Z
hook:    wired in ~/.claude/settings.json
shadow:  off

Exits non-zero when something is actually broken (unreadable or invalid rules, unusable store); a missing rules file is just reported, since no rules means isthmos is a deliberate no-op.

As a Go library
import "github.com/sphragis-oss/isthmos"

rs := isthmos.LoadRules(path)
out, changed := isthmos.Apply(rs, toolName, rawOutput)

Configuration

Rules live in ~/.config/isthmos/rules.json (override with ISTHMOS_RULES). Tool names are glob-matched, listed keys are dropped recursively:

{
  "rules": [
    {"tool": "mcp__github__*", "drop_keys": ["node_id", "avatar_url"], "max_items": 30},
    {"tool": "mcp__*", "max_str": 8000}
  ]
}

Besides drop_keys, a rule can cap payload size generically: max_items truncates any array beyond N elements and max_str truncates any string beyond N bytes (at a rune boundary). Both replace the removed tail with an explicit [isthmos: ... truncated] marker so the model knows the payload is partial; truncation is never silent. When several rules match, the strictest positive limit wins.

Truncation is head-and-tail, not naive: keep_last reserves part of the max_items budget for the newest entries, and items that look like errors (a truthy error field, or status/level/conclusion values such as failed or fatal) are always kept regardless of position, because those are the items an agent is usually looking for. min_bytes gates a whole rule: payloads smaller than it pass through untouched, so tiny outputs are never rewritten.

See rules.example.json for a starter set covering Atlassian and GitHub MCP noise fields. No config means no rewriting: isthmos is fail-open and only ever emits a replacement when the result is strictly smaller.

Measurement

Every invocation appends one line to ~/.local/state/isthmos/measure.jsonl with before/after byte counts per tool, so pruning rules are driven by real data, not guesses. isthmos stats turns that log into a savings table:

$ isthmos stats -since 168h
TOOL                                      CALLS  IN     OUT    SAVED  SAVED%  ~TOKENS
mcp__atlassian__searchJiraIssuesUsingJql  42     1.9MB  0.6MB  1.3MB  68.4%   340787
mcp__github__get_me                       7      12.3KB 4.1KB  8.2KB  66.7%   2099
TOTAL                                     49     1.9MB  0.6MB  1.3MB  68.4%   342886

-file points at a different log, -since bounds the window. The ~TOKENS column is a rough 4-bytes-per-token estimate, not a tokenizer.

Shadow mode

Set ISTHMOS_SHADOW=1 to measure without rewriting: isthmos computes what the rules would save and logs it, but the hook emits nothing and filter passes stdin through untouched. Nothing is written to the reversibility store. Use it to trial rules on a new machine, then unset it once isthmos stats shows the savings are worth it:

{"type": "command", "command": "ISTHMOS_SHADOW=1 $HOME/.local/bin/isthmos hook", "timeout": 5}

Reversibility

Truncation is reversible. When items or bytes are cut, the original payload is encrypted (AES-256-GCM) into ~/.local/state/isthmos/store/ and the marker carries the recovery command:

[isthmos: 17 of 20 items truncated, full: isthmos reveal de6ac0410501901b]

An agent that needs the full payload can simply run that command; a human can too. Entries expire after 7 days. If the store cannot be written, isthmos does not truncate at all: a marker must never point at a payload that was not stored. Field-pruned payloads (no truncation) are not stored.

Design constraints

  • No proxy in the credential path
  • One static binary, fast cold start (runs on every tool call)
  • Fail-open: any error means untouched passthrough
  • Lossy steps must be reversible or clearly labelled

Contributing

Issues and pull requests are welcome. See CONTRIBUTING.md for setup, style, and the DCO sign-off requirement, and GOVERNANCE.md for how decisions get made. Security reports go through SECURITY.md, never a public issue.

Pruning rules are the highest-value contribution: bring one backed by real before/after byte counts.

License

Apache-2.0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Apply

func Apply(rs Rules, tool string, output json.RawMessage) (json.RawMessage, bool)

Apply returns the possibly pruned output and whether it shrank

func ApplyWithStore

func ApplyWithStore(rs Rules, tool string, output json.RawMessage, st *Store) (json.RawMessage, bool)

ApplyWithStore also spools the original payload so truncation markers are reversible

func EstTokens

func EstTokens(b int64) int64

EstTokens is a rough 4-bytes-per-token estimate, not a tokenizer

func PruneJSON

func PruneJSON(raw json.RawMessage, drop map[string]bool, lim Limits) ([]byte, error)

PruneJSON drops keys and applies limits recursively, unwrapping a JSON-encoded string payload

Types

type Limits

type Limits struct {
	MaxItems int
	MaxStr   int
	KeepLast int
}

type Measure

type Measure struct {
	TS       time.Time `json:"ts"`
	Tool     string    `json:"tool"`
	InBytes  int       `json:"in_bytes"`
	OutBytes int       `json:"out_bytes"`
}

type Rule

type Rule struct {
	Tool     string   `json:"tool"`
	DropKeys []string `json:"drop_keys"`
	MaxItems int      `json:"max_items,omitempty"`
	MaxStr   int      `json:"max_str,omitempty"`
	KeepLast int      `json:"keep_last,omitempty"`
	MinBytes int      `json:"min_bytes,omitempty"`
}

type Rules

type Rules struct {
	Rules []Rule `json:"rules"`
}

func LoadRules

func LoadRules(p string) Rules

LoadRules is fail-open: missing or bad config means no rules

func (Rules) DropFor

func (rs Rules) DropFor(tool string) map[string]bool

DropFor merges drop keys from every rule whose glob matches the tool name

func (Rules) LimitsFor

func (rs Rules) LimitsFor(tool string) Limits

LimitsFor takes the strictest positive limit across matching rules

type Store

type Store struct {
	// contains filtered or unexported fields
}

func OpenStore

func OpenStore(dir string, ttl time.Duration) (*Store, error)

OpenStore prepares the reversibility store, creating dir and key on first use

func (*Store) Load

func (s *Store) Load(id string) ([]byte, error)

Load opens the sealed payload for id

func (*Store) Save

func (s *Store) Save(id string, payload []byte) error

Save seals the payload under id and opportunistically drops expired entries

type ToolStat

type ToolStat struct {
	Tool     string
	Calls    int
	InBytes  int64
	OutBytes int64
}

func Aggregate

func Aggregate(r io.Reader, since time.Time) []ToolStat

Aggregate sums measurements per tool, skipping lines it cannot parse

func (ToolStat) Saved

func (s ToolStat) Saved() int64

Directories

Path Synopsis
cmd
isthmos command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL