-
Notifications
You must be signed in to change notification settings - Fork 0
Shadowing
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 server declares what it shadows, in its own config entry:
| 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.
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.
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.
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.
mduct shadowserver 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.
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.