Skip to content

Repository files navigation

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-run

Works 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 7d

Claude 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 (default 5m) → fresh run
  • Full logs kept on disk under .cache/cached-run (or CACHED_RUN_CACHE_DIR)
  • --force or CACHED_RUN_FORCE=1 bypasses 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 HEAD commit
  • 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-run prints 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:

  1. Parses the command and matches it against your configured patterns.
  2. If it doesn't match — or can't be parsed safely (see Safety) — exits silently, and Claude runs the original command untouched.
  3. If it matches, emits a PreToolUse decision that rewrites the command to <cli> run -- <command>, reusing the exact binary that ran the hook (so there's no PATH dependency; binary in 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-run captures stdout and stderr together into one combined log file.
  • On a cache hit, cached-run returns 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 -50 sees the cached-run summary on cache hits, not the command log.
  • Use the LOG: path from the summary to inspect the full cached output.

About

Cache expensive verification commands so coding agents do not rerun them for the same source state.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages