cached-run
v0.0.1
Published
Cache expensive verification commands so coding agents do not rerun them for the same source state.
Downloads
259
Maintainers
Readme
cached-run
Cache expensive verification commands so coding agents can't run the same check twice for the same source state.
Install
npm install -D cached-runWorks with any package manager (pnpm, yarn, bun); the examples below use npm.
Usage
cached-run run -- npm run typecheck
cached-run run --shell "npm run typecheck"
cached-run explain --shell "npm run typecheck"
# or one quoted argument (argv often contains flags the CLI would misread)
cached-run explain "npm run typecheck"
cached-run clean --older-than 7dClaude Code hook
Add to .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "npx cached-run hook claude"
}
]
}
]
}
}Matching Bash commands are rewritten to node <absolute-path-to-this-cli> run -- … using the same binary that ran the hook (no PATH or extra config required). Optional binary in config overrides that invocation.
Guarantee
- Same command + same source state → cached result (original exit code)
- Changed source state → fresh run
- Cache entry older than
ttl(default5m) → fresh run - Full logs kept on disk under
.cache/cached-run(orCACHED_RUN_CACHE_DIR) --forceorCACHED_RUN_FORCE=1bypasses cache
How it works
On each run, cached-run derives a key from two things: the command and the state of your source tree.
The command — the parsed argv, your config (cache dir and command patterns), and cached-run's own version. Upgrading the tool or editing config invalidates earlier entries.
The source state, read from git:
- the current
HEADcommit - a diff of every tracked file against
HEAD, so uncommitted edits count - a content hash of every untracked file that isn't gitignored
These are folded into a SHA-256 digest (truncated to a short hex key). Two runs share a key only when the command and every byte of relevant source match.
From there:
- Miss — the command runs. Combined stdout and stderr are captured to a log under
cacheDir, with metadata (exit code, duration, error count, timestamp). The command's real exit code is returned. - Hit — the key matches an entry within
ttl, so the command is skipped entirely;cached-runprints a summary and returns the original exit code.
Only commands matching a configured pattern are cached; the cache is just files on disk under cacheDir, with no daemon or global state.
Through the Claude Code hook
The hook is a PreToolUse rewriter, not a wrapper — it never runs your command or touches the cache itself. On each Bash tool call, Claude Code pipes the pending command to cached-run hook claude, which:
- Parses the command and matches it against your configured patterns.
- If it doesn't match — or can't be parsed safely (see Safety) — exits silently, and Claude runs the original command untouched.
- If it matches, emits a
PreToolUsedecision that rewrites the command to<cli> run -- <command>, reusing the exact binary that ran the hook (so there's no PATH dependency;binaryin config can override it).
Claude then runs that rewritten command, and the caching above takes over.
Config
Create cached-run.config.json (or .mjs / .js / .ts):
{
"cacheDir": ".cache/cached-run",
"ttl": "5m",
"commands": [
"pnpm -r typecheck",
"pnpm --filter * typecheck"
]
}Each command is a shell string. * matches exactly one whitespace-separated
segment (e.g. a package name); a pattern must match the whole command.
Safety
Conservative by design: redirects, &&, env prefixes, subshells, and already-wrapped commands pass through unchanged. Simple matching commands rewrite to cached-run run -- …. If the agent adds a single output pipe (| tail, | head, | grep), the hook prefixes cached-run and keeps the pipe: cached-run run -- npm test | tail -100. Exit codes through pipes are best-effort.
Caveats
cached-runcaptures stdout and stderr together into one combined log file.- On a cache hit,
cached-runreturns the original exit code and prints a short summary. It does not replay the cached command output to stdout. - Because cache hits print the summary instead of the original output, a trailing pipe such as
| tail -50sees thecached-runsummary on cache hits, not the command log. - Use the
LOG:path from the summary to inspect the full cached output.
