DEV Community

Cover image for Progressive Disclosure: Shaping Claude Code's Output
Gábor Mészáros Subscriber for Reporails

Posted on Originally published at reporails.com

Progressive Disclosure: Shaping Claude Code's Output

Opus 5.5 shipped on September 22, and its replies read differently: answer first, less throat-clearing. Box, testing it before launch, found its answers "40% less verbose without losing accuracy", and Factory measured "20 to 25% fewer output tokens" (Introducing Claude Opus 5.5). Those are two teams' numbers on their own workloads, and they point the same way.

The shape of the reply is still yours to set. A shorter default helps the rename and shortchanges the design question, and what you actually want is both: two lines when you ask for a rename, the full walkthrough when you ask whether the cache belongs in Redis, on every turn, without saying which each time. That makes it a loading problem. The instruction that shapes the reply has to be in front of the model on the turn it applies and out of the way on the others.

The first piece in this series split progressive disclosure into three handles: what loads, where it loads, and when (Progressive Disclosure: What, Where, When, and Why). This one takes a single concern, the shape of the reply, and walks it up through every place Claude Code will load an instruction from. Each step gets further than the last and ends on its own limit. Then we go one level up.

Output style: one shape for the whole session

Claude Code has a setting for exactly this. An output style is "a set of instructions that sets Claude's role, tone, and response format for every response in a session", and the built-in Concise style is pure reply shape: the first sentence states the answer, the lead-in, the step-by-step narration and the closing recap go, a simple question gets one to three sentences, and the engineering work stays as thorough as in the default style (output styles). Pick it with /output-style concise, or write your own as a markdown file in .claude/output-styles/.

The reason to reach for it is where it sits. The style's instructions travel with the system prompt on every request. Your CLAUDE.md "is delivered as a user message after the system prompt, not as part of the system prompt itself" (memory), and Anthropic's prompting guide, for this exact problem, says to "use direct instructions in the system prompt: 'Respond directly without preamble'" (prompting best practices). The rule reads the same in both places; the output style gives it the higher seat.

The request the model reads: tools, then the system prompt, then messages. The output-style rule is lit inside the system prompt. The same rule sits dimmed in the CLAUDE.md user message after it, next to the MEMORY.md index line loaded at session start.

This is the lean root from the first piece: a rule that applies to every reply earns its always-on slot. Its limit is built into the definition, though. It's one shape for the whole session, so the rename and the design question get the same one.

Path rules: triggered by the file

Rules that apply only sometimes go in .claude/rules/, one topic per file. Give a rule a paths field and it becomes conditional: path-scoped rules "trigger when Claude reads files matching the pattern", and stay out of context otherwise (memory).

---
paths:
  - "docs/**"
---
When answering about documentation, open with the context the reader needs, then the change.
Enter fullscreen mode Exit fullscreen mode

That rule shows up when Claude reads a file under docs/ and stays out of the rename in src/app/nav.tsx. What triggers it is the file the agent touches. A design question can arrive while the agent has any file open, and no glob tells a design question from a rename.

A path-scoped rule whose paths field matches a directory. On a turn reading a file outside it the rule stays out of context; on a turn reading a matching file it loads.

Auto memory: the model decides what loads

Auto memory is the built-in handle that comes closest to loading on relevance. Claude saves "corrections you give Claude and approaches you confirm" into one directory per project, ~/.claude/projects/<project>/memory/. The first 200 lines (or 25KB) of the MEMORY.md index load at the start of every session. The topic files behind it don't: Claude "reads them on demand using its standard file tools when it needs the information" (memory). Correct the shape of a design answer a couple of times and you may find a one-line entry, design answers: trade-offs first, with the full note behind it.

The MEMORY.md index is loaded every session, one entry reading design answers: trade-offs first. On a turn asking to rename a function, the topic file behind that line stays unread. On a turn asking whether to cache in Redis or in memory, Claude reads it and the full reply-shape note enters context.

The index is always there and the detail comes in when it's needed. Which detail, and when, is Claude's call, made from a line Claude wrote. You can open and edit every one of these files with /memory, but by default Claude does the writing, and the docs call auto memory "machine-local": the laptop that learned how you like design answers is the only machine that knows.

UserPromptSubmit: you decide, inside a regex

To take the routing back, Claude Code runs a UserPromptSubmit hook on every message before Claude sees it. The hook gets your text in the prompt field on stdin, and plain stdout on a clean exit is added as context Claude can see (hooks). So you classify the turn yourself, in .claude/hooks/reply-shape.sh:

#!/usr/bin/env bash
# UserPromptSubmit hook: add the reply shape that fits this kind of turn.
prompt="$(jq -r '.prompt // ""')"

if grep -qiE '\b(why|should we|trade-?offs?|design|architecture|compare)\b' <<<"$prompt"; then
  echo "Reply shape for this turn: a design question. Lay out the options and their trade-offs, then recommend one."
elif grep -qiE '\b(rename|typo|bump|move|delete)\b' <<<;"$prompt"; then
  echo "Reply shape for this turn: a routine edit. Make the change, then answer in one or two sentences."
fi
exit 0
Enter fullscreen mode Exit fullscreen mode

Wire it under hooks.UserPromptSubmit in .claude/settings.json. The event takes no matcher, so it runs on every prompt:

{
  "hooks": {
    "UserPromptSubmit": [
      { "hooks": [{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/reply-shape.sh" }] }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

The flow of a UserPromptSubmit hook: your message goes to reply-shape.sh, which reads the prompt field. A design question adds the line lay out the trade-offs, then recommend. A routine edit adds the line one or two sentences. Anything else adds nothing. Claude then answers with that line in context.

Now the decision is yours, made the same way on every turn. Look at what happened to the rule on the way. "Lay out the trade-offs, then recommend" is a string inside an echo, its condition is a regex two lines above it, and whether any of it runs is decided in a JSON file somewhere else. "Rename the design tokens" matches both lists, and the first match wins.

Stop: a check after the fact

The last handle runs once the reply is written. A Stop hook receives the finished reply as last_assistant_message; exit with code 2 and the stop is blocked, with your stderr fed back to Claude to try again. stop_hook_active is true when a Stop hook already sent the reply back, which is your loop guard (hooks). Save this one as .claude/hooks/gate-length.sh and wire it under hooks.Stop:

#!/usr/bin/env bash
# Stop hook: send an over-long reply back once.
input="$(cat)"
[ "$(jq -r '.stop_hook_active // false' <<<"$input")" = "true" ] &amp;&amp; exit 0

words="$(jq -r '.last_assistant_message // ""' <<<"$input" | wc -w)"
if [ "$words" -gt 180 ]; then
  echo "Your reply ran ${words} words; the budget is 180. Put the result first and cut the recap." &gt;&amp;2
  exit 2
fi
exit 0
Enter fullscreen mode Exit fullscreen mode

The Stop hook loop: the model finishes a turn, the hook reads the reply from last_assistant_message, then checks it against the 180-word budget. Over budget, it exits 2 and sends the reply back once; within budget, it exits 0 and the turn ends. The stop_hook_active guard blocks once, then stands down.

This one is a check. It loads nothing into the model's context before the reply; it measures what came out and sends it back. It also shares the output style's blind spot: 180 words is the budget for the rename and the design question alike.

Where the harness comes short

This claim is easy to overstate, so here it is precisely. The harness does load on relevance: memory topic files and skills come in when they're needed. Rules can declare paths. A hook can route by the kind of turn, as reply-shape.sh does. Each handle did its job. What the harness does is deliver rules. Organizing them is left to you, and that shows up in four places:

  • A rule's text and its loading condition live apart. The design-answer rule is an echo string, its condition is a regex in the same script, and its switch is an entry in settings.json.
  • Loading conditions come in five forms. Always on, a path glob, the model's judgment, a regex on the prompt, a check on the finished reply. Each form follows what the harness exposes at that point (a file read, the prompt text, the reply), and none follows the situation you care about, which is what kind of turn this is.
  • Nothing lists every rule with its condition, and nothing tests whether two collide. The output style says result first; the docs rule says context first; on a design question about the docs, both are in the window. The model picks one and raises no error (Opus 5: Cost of Instruction Conflicts).
  • Nothing lets the reply's shape change with the kind of turn and bring its own checks along. The Stop hook's budget has no idea the turn was a design question.

Six cards for one concern: the output style, a docs rule, CLAUDE.md, two hook scripts in the repository, and the Claude-written MEMORY.md on one machine. Amber links mark two pairs that cannot both hold: result first against context first, and one or two sentences against the full walkthrough.

One level up: the reply declares its kind

So we moved the problem up a level in our own setup. The agents that build Reporails open every reply with a declared kind: [status] for a work report, [analysis] for reasoning, [narration] for how something works, [handover] for the brief a session hands its successor, plus domain tokens such as [surface] when the work is the public site. That one token decides two things: which reply-shape rules apply, and which checks run when the reply ends.

In practice, [narration] relaxes the check that counts separate blocks in a reply, since an explanation reads as paragraphs, and keeps the 150-word length check. [handover] relaxes seven checks that a recap of finished work would trip by construction, and the check that stops a reply from handing new work back to me still runs. [surface] switches on a periodic re-read of our visual-review rules, but only while that domain is the current work, so a research session never pays for it. A mislabelled reply still hits every check its label doesn't relax, so the token scopes the checks without becoming a way around them.

A reply opens with a declared kind. The kind selects its row in a class map kept outside the harness. Narration: block-count check relaxed, 150-word length check on. Handover: seven report checks relaxed, the no-work-pushed-to-the-operator check on. Surface: a periodic re-read of the visual-review rules, on only while the domain is current. One Stop hook delivers the selected rules and checks.

That's output style done as progressive disclosure. The shape rules load according to the kind of turn, the rules and the map from kind to rules live outside the harness as plain files, and the harness only delivers them, through one Stop hook. Each of those files states the situation it applies to next to its own text, which also covers rules that no harness handle can express directly. One of ours reads, in effect, "load this before the agent writes a file in our knowledge base". Path rules fire when Claude reads a matching file, and the moment we need comes before it writes one. You could build that with a PreToolUse hook, with the glob in yet another script. In our setup the condition sits in the rule's own header.

One Stop hook is the naive version of this. Look at what it needs underneath. Each rule names the moment it applies to: before a write to the knowledge base, at the end of a reply of a given kind, every few turns while a domain is the current work. The harness fires its own events (a prompt arrives, a tool is about to run, a reply ends), and none of them is one of those moments. Something has to sit between the two, turning each harness event into the moment it represents and handing over only the rules that asked for that moment. That something is an event bus, and why a rule set outgrows its harness without one is the next piece.

Once the rules live one level up, the question you ask changes from where a rule goes to what the whole set does on this turn.


I work on Reporails, deterministic diagnostics and governance for the instruction files, rules, and prompts that steer coding agents. It reads the steering surface you wrote down and tells you, with measured evidence, which instructions couple to behavior and which are text the model will ignore.

Top comments (4)

Collapse
 
reidmarlow profile image
Reid Marlow

Self-declaring the turn kind with tags like [status] or [analysis] fixes the prompt regex problem, but the edge that bites in longer agent loops is state drift mid-turn. An agent starts a task assuming a routine three-line fix under [status], executes a bash tool that fails an unexpected integration test, and suddenly needs an architectural pivot. If the response budget locks on that opening tag, the model truncates the explanation to squeeze under the status check. Allowing the model to emit a revised intent token after tool execution if the turn shifts from execution to debugging saved us a lot of retried turns.

Collapse
 
cleverhoods profile image
Gábor Mészáros Reporails

agree, locking the budget on the opening intent would bite exactly there. in our setup the tag goes on the final reply rather than the task, so it's declared after the tool calls ran: the routine fix that hit a failing integration test just ends as [status][analysis], and a few checks (length among them) bind whatever the tag says. do you re-run the checks after each revised token or only on the last one?

Collapse
 
brianainews profile image
Brian · AI News

Progressive disclosure is the right antidote to dumping an entire repository into every prompt. The strongest pattern here is letting file context and task intent decide what becomes visible, which should reduce both noise and accidental instruction conflicts. I would love to see a small trace view that shows which rule shaped each response.

Collapse
 
cleverhoods profile image
Gábor Mészáros Reporails • Edited

We'll get there too together with the event bus in the next article. The pre-requisite was the self-classification and what it entails.