Skip to content

Shadowing

Kevin Straub edited this page Aug 1, 2026 · 5 revisions

A prompt block is read once at the start of a session and then loses to habit.

Measured on one real two-day session: 21 calls to a configured code-index server against 270 grep invocations into the very repositories that server had indexed — with the index block present in context the entire time. The agent knew. It reached for grep anyway, because grep is a loaded tool and the other thing is a line of prose.

Shadowing moves the reminder to the only place that reliably works: the moment the other tool is about to run.

A rule

A server declares what it shadows, in its own config entry:

"codeindex": {
  "command": "",
  "shadow": [{
    "tool": ["Grep", "Glob"],
    "bash": "(?:^|[\\n;]|&&)\\s*(grep|rg|ugrep)\\b",
    "pathIn": ["~/src/bigrepo", "~/src/other"],
    "hint": "That repo is indexed. `mduct call codeindex search query=…` finds it with surrounding context.",
    "budget": 2,
    "refillMin": 30
  }]
}
Field Meaning
tool Exact tool names to shadow, as the harness names them.
bash Regex against a Bash command.
pathIn Gate: only fire where this server is actually useful. Matched against the command text and the directory the command runs in.
hint What the user sees. Put the runnable command here, not advice.
budget Bucket capacity — how many hints may land back to back. Default 1.
refillMin Minutes to refill one hint. Default 0 = never refills.

mduct knows nothing about what any server does. It matches the patterns and quotes the hint. Delete the server from the config and the mechanism is inert.

It is a redirect, not a ban

The hint arrives as a denial, because that is the one channel guaranteed to reach the model, and the message says plainly that the tool is not blocked: running the same command again works. Roughly half of all grep calls are the right call — a rule that stopped them would be worse than no rule.

The bucket

A per-session counter sounds right until you look at a real session: two days, 816 tool calls. One hint at call five is an accident, not a lesson.

So it is a token bucket. budget allows a short burst, refillMin brings hints back while the session continues. With budget: 2, refillMin: 30 a long working day gets roughly fifteen to twenty opportunities instead of one — still nothing against 270 greps, but enough to shape a habit.

refillMin: 0 is a plain fixed budget per session, and it is the default.

Write the regex carefully

ps aux | grep x filters the output of a command. It does not search a repository, and a rule that fires on it is pure friction.

Anchor to a command start:

"bash": "(?:^|[\\n;]|&&)\\s*(grep|rg|ugrep)\\b"

Measured against one session's 837 Bash calls: the naive \bgrep\b matched 340 commands where only 255 were real searches. The other 85 were ps aux | grep, docker ps | grep, tail … | grep, tsc | grep -v — noise, every one.

The same applies to pathIn. A session's working directory is often a fixed project root, so a rule gated only on it fires for commands that reach entirely elsewhere. mduct follows a leading cd in the command for exactly this reason, but keep the gate as narrow as the server's usefulness actually is.

Measure it

mduct shadow
server         nudges  converted
  codeindex         14          9

23 events — /home/you/.cache/mduct/shadow.jsonl

A nudge counts as converted when a call to that server follows it in the same session. If converted stays far below nudges, the hint is being ignored and you are buying friction with nothing in return — change the hint, narrow the rule, or drop it.

The log is plain JSONL, one line per event. Delete it to start a clean measurement.

Wiring

The PreToolUse matcher is derived from your rules at mduct hook install claude time, so a config with no shadow block costs nothing — no extra process per Bash call. Change your rules and the next session start tells you if the installed matcher no longer covers them.

Clone this wiki locally