Files
deepseek-harness/docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.md
T
Tianyi Cui 09f131f78e docs(rfc): fold PR #139's simplification sweep into this set
Deduplicate the parallel sweep from codex/exhaustive-simplification-rfcs
(merged in the parent commit) against the twelve RFCs already here, on
the merits per item:

Ported (rewritten to this set's evidence standard):
- fold-stdio-ui-helper — verified: dsh-stdio-agent is the only runtime
  importer; the fold resolves the documented product-app-depends-on-
  support-package tension. The false acp-agent analogy is replaced with
  the real distinction (protocol product surface vs one app's front-door
  scaffolding).
- generate-rfc-index-tables — verified: the classification RFC records
  rejecting generation; the index tables are the repo's only
  systematically conflicting docs region. Process framing made timeless.

Consolidated into existing owners:
- web searchStatus()/fetchStatus()/WebCapabilityStatus (verified: zero
  production callers; the tool-web README and architecture.md claims are
  drift) → drop-web-providers-change-event, renamed
  drop-unconsumed-web-observation-surface.
- hook/result.durationMs (unread, nondeterministic, normalizer-scrubbed)
  and the double-defaulted defaultTimeoutMs knob →
  tighten-hook-protocol-contract.
- the exercised-but-unadvertised exec.arguments mutation path (a
  tool-bash integration shim rewrites through it) → a sanction-or-seal
  note in the pre-tool-input-rewrite proposal.
- the dormant-guard critique of subagent depth machinery → recorded in
  prune-unimplemented-subagent-vocabulary as the considered-and-rejected
  alternative, with the keep sharpened (uncapped-today acknowledged;
  wiring the cap is the completion, not deletion). getProvider()/list()
  and lastAssistantMessage recorded as examined-and-kept (bash-revert
  precedent; observe-enrich recorded keep).

Not ported (with reasons):
- tools/change + system-prompt/change removal — recorded keeps in the
  adapter-change RFC, unengaged by the sweep; no new facts.
- LlmService.models() — flagged by both surveys, but two lines with a
  plausible consumer: TODO-or-drive-by territory per the RFC bar, not
  a proposal.
- SchemaProp.default RFC — already XXX(unused-default)-tagged; the RFC
  bar excludes TODO-tracked provisional cleanups.
- PreToolDecision 'ask' removal — FIXME(permissions)-anchored deferral
  with the permission system as its named consumer.
- Codex bridge Config.model, merged systemMessages — wire-faithful
  tested surface / README-documented deferral.

Their in-code TODO notes (acp-test-harness, hook-snapshot-noise,
catalog-type-links) and the stale hook-prompt-block name fixes ride the
merge unchanged.
2026-07-04 12:53:21 +08:00

4.0 KiB

RFC: Pre-tool input rewrite — a consistent design (proposed)

Status: proposed (2026-06-30)

Context

The interception-seams RFC added tools/pre-execute returning a PreToolDecision (allow/deny/ask) — but deliberately NOT input rewrite (a hook changing a tool call's arguments before it runs). Claude Code's PreToolUse hook offers an updatedInput, so a faithful CC bridge wants the same. This RFC designs that, separately, because doing it consistently is a real problem — not a field to bolt onto the allow decision.

The problem: three readers of pre-execution arguments

In the loop, a tool call's arguments are committed to the log and read by live consumers BEFORE the tool executes:

  1. assistant/message is appended before tool dispatch — it is the model-history source deriveMessages() replays, so it carries the tool-call arguments the model itself emitted.
  2. tool/call is the durable AUDIT record, appended before ctx.tools.execute().
  3. Live presentation reads tool/call.arguments: the ACP bridge remembers them and passes them to presentResult; dsh-tool-bash derives the card title, the rawInput, the cwd, and the terminal-vs-background treatment from them.

So an "input rewrite" that changes ONLY what executes would make the UI show one command while another RAN, and render result state against the wrong arguments — a real inconsistency, not a documentable gap. (The existing low-level capability to mutate exec.arguments in a listener has exactly this latent inconsistency; it is unadvertised precisely because of this — yet not unused: a tool-bash integration test rewrites a scripted call's arguments through it (packages/bash/tool-bash/tests/integration.spec.ts), so this design must either sanction that path with the consistency unit below or seal it — readonly arguments at the seam, with the test shim moved onto a behavior-level helper.)

Proposed design (sketch — to validate against the code when built)

Treat input rewrite as a consistency unit: when a pre-execute hook supplies updatedInput, the rewrite must be reflected in ALL three readers, atomically, before execution:

  • The tool/call audit event records the REWRITTEN arguments (with the original retained in a sidecar field for the audit trail — a hook changed the call, and both the original and the effective arguments are facts worth keeping).
  • The assistant/message in derived history must agree with what executed — options to evaluate: rewrite the assistant message's tool-call block in place (changes what the model "sees it said"), or record a separate correction the next request carries. The CC model is that the model sees the rewrite took effect.
  • Presentation (presentCall/presentResult) reads the rewritten arguments, so the UI shows what actually ran.

The shape would extend PreToolDecision with an allow-variant arguments (or a dedicated {kind:'rewrite', arguments}), and the loop would thread the rewrite through the three readers above rather than only into ctx.tools.execute().

Why not now

The interception-seams RFC notes input rewrite "fought the code across two review rounds" — the signal AGENTS.md names for an over-reaching change. Shipping allow/deny/ask first keeps the seam honest (no advertised contract that silently desyncs the UI), and a CC/Codex bridge that receives an updatedInput logs it and surfaces a faithful-but-degraded warning (like ask→deny) until this lands. This RFC is the home for the consistent design; TODO(pre-tool-input-rewrite) in the loop's pre-execute call site anchors it.

Open questions

  • Does rewriting the assistant/message tool-call block corrupt any provider's expectation on replay, or is a separate correction safer?
  • Should the original arguments be preserved on the tool/call event (audit) and, if so, under what field?
  • How does this interact with a future permission ask flow (a user approving a rewritten call)?