How to Steer the model via tool call to improve Harness Quality
Do this first: pick one rule from your system prompt that a script could check. Delete the line. Enforce it at the tool call instead. That is the whole idea. The rest of this post shows how OpenHands SDK, OpenCode, Herm
Do this first: pick one rule from your system prompt that a script could check. Delete the line. Enforce it at the tool call instead.
That is the whole idea. The rest of this post shows how OpenHands SDK, OpenCode, Hermes Agent, DeepSeek harness and Strands do it.
Why the tool call beats the prompt
- A prompt rule is read once and fades as the conversation grows.
- A rejected tool call arrives at the exact moment of the mistake, about the exact action, at the end of the context.
- The model doesn't have to remember the rule. It only has to read the reply.
The steering loop: four steps
Every harness above runs the same loop on each tool call:
- Detect. Check the call against a rule a script can decide: was the file read, is this a repeat, is the budget spent, is the path allowed.
- Reject. Don't run it. Return a tool result marked as an error.
- Name the next move. Say why it failed and what the legal next action is.
- Let the model retry. It reads the reply on its next turn and corrects itself. No prompt line needed.
Step 3 is the one people skip. A bare "denied" makes the model guess, and it guesses by sending the same call with a small change.
Four checks to build first
-
Read before edit. Keep a record of what was read. Refuse an edit to an unread or changed file. DeepSeek:
edit requires reading "X" first(fs-observation-policy). - Repeated calls. Count calls with the same tool and arguments. DeepSeek reminds at 3, 5 and 8 repeats: "Do not call this tool with these exact arguments again" (repeat-tool-reminder). Hermes blocks at 5: "Stop retrying it unchanged; change strategy or explain the blocker" (tool_guardrails.py).
-
The step budget. When it's spent, OpenCode turns tools off (
toolChoice: "none"), appends a wrap-up message, and refuses any call that still arrives (llm.ts). The run ends with an answer. - Dangerous paths and commands. Parse the command before running it. OpenCode checks each command and each target folder as its own permission (shell.ts). Hermes keeps a blocklist no mode can override: "not even with --yolo" (approval_floors.py).
Example: PDFs
Your agent keeps trying to read PDFs as text. Two ways to fix it in the harness, no prompt line:
-
Reject the wrong route. The model runs
cat paper.pdfor a shell one-liner to dump it. The shell tool spots the.pdftarget and refuses: "PDFs can't be read as text. Use theparse_pdftool." The model switches tools. -
Do the work instead of refusing. The file-read tool gets a PDF. Instead of an error, the harness parses it, saves the text as
paper.md, and replies: "Parsedpaper.pdf. Readpaper.md." The model never sees the failure.
Option 2 is better whenever the harness can do the job itself. It moves the work from the model to code.
DeepSeek uses the same trick for tools that must be called from code: "only run_code is callable directly — call X from inside a run_code program instead". Its comment explains why: with a bare "unknown tool", the model concluded the deployment was broken (core/tools).
Messages that steer
Each one answers why and what next:
- "Cannot use edit to modify files outside the Plan directory" (OpenCode plan.ts)
- "Delegation depth limit reached … Complete this task yourself instead of calling subagent again" (Strands subagent.py)
- "Do NOT retry this command, do NOT rephrase it … wait for the user" (Hermes approval.py)
- "Re-send WITHOUT the wrapper as terminal(…, background=true)" (Hermes terminal guards)
- "Did you mean one of these?" for a missing file (OpenCode read.ts)
Hooks: steering without touching tool code
Hooks run your check before or after any tool call:
- PreToolUse can deny with a reason. DeepSeek turns a hook's deny into a tool denial the model reads (hooks-claude-code).
- Exit code 2 blocks. OpenHands follows Claude Code's convention: a hook script that exits 2 stops the call (hooks/types.py, hooks/manager.py).
- Hooks can add context instead of blocking. Hermes hooks return JSON to block, rewrite the input, or attach a note (shell_hooks.py).
- A Stop hook can refuse "done". DeepSeek and OpenHands feed the hook's reason back and keep the run going.
"Isn't this custom code?"
No. It is the harness doing its job. A read ledger, a call counter, a path check, a hook runner: these are why OpenCode, Hermes and the OpenHands SDK each run past 20k lines outside their prompts. The model supplies judgement. The harness supplies the guard rails, and the guard rails are most of the code.
Where to draw the line
- Something the environment can make impossible: change the environment.
- A rule a script can check: enforce it at the tool call, reply with the next move.
- Judgement (what to search, how to write, when to ask): leave it to the prompt.
Next: open your system prompt and count the lines that start with "never" or "always". Each one is a candidate for step 2.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.