Natural Language Tool State
Background
This observation comes from a screenshot about Claude's bash command style. The core claims:
- When Claude generates a bash command, it often writes a comment line first, then the actual command.
- It also frequently uses
echo, or&& echo "success" || echo "failed", to translate exit codes into natural language. - These seemingly redundant bits of text are actually exploiting natural language as the state representation a model consumes most easily.
- When designing agent tool use, tool input can carry a description / intent first, and tool output can return natural-language state rather than bare data only.
What checking the claims found
The direction is right, but it splits into three layers.
Layer one: bash comments are indeed ignored by the shell, but they stay in the context the model has already generated. The GNU Bash manual states that in a non-interactive shell, a word beginning with # causes that word and all remaining characters on the line to be ignored. So when the model generates a comment line before the command, the comment doesn't change shell execution — but it pins the intent first, inside the model's own generation trajectory.
Layer two: natural-language intermediate state helping model action has paper and engineering-doc support. Chain-of-Thought shows intermediate reasoning steps improve complex reasoning; ReAct goes further and interleaves reasoning traces with actions, letting the reasoning trajectory help the model track and update plans and handle exceptions. That explains why "first say in one plain sentence what you're about to do" tends to make the following tool call more stable.
Layer three: the natural-language quality of tool descriptions and tool output genuinely affects model performance. Anthropic's docs say explicitly that a tool description should explain what the tool does, when to use it, what the parameters mean and its limitations, and call a detailed description an important factor in tool performance. OpenAI's function calling docs likewise note that tool definitions are injected into the model's context, and that function output is usually returned as a string — JSON, an error code, or plain text — which the model interprets as needed.
So the more accurate statement is not "natural language beats structured data", but:
For an LLM, structured state carries executability; natural-language state carries explainability and a prior for the next action.
A point that needed correcting
The screenshot claims the OpenCode bash tool has a description parameter. That example needs correcting.
I checked the current opencode-ai/opencode source (2026-05-22, commit 73ee493) and README: the built-in bash tool's parameters are command and timeout; there is no per-call description field. It does have a long tool-level description, the command parameter has its own description, and the system prompt asks the model to explain what a non-trivial bash command does and why before running it.
So OpenCode's real design is closer to:
- Front-load semantics into the tool schema / tool description.
- Have the model explain non-trivial commands in the assistant prose.
- Keep the bash tool input itself centered on
command.
If we design our own custom bash tool, we can still add a description / intent field — but it can't be presented as an existing fact about OpenCode's built-in bash tool.
Design suggestions
Tool input
For tools that are high-risk, long-running, or mutate system state, add an explicit intent or description field to tool input:
{
"intent": "检查 dev server 是否被 3000 端口占用,并找出占用进程",
"command": "lsof -i :3000"
}
This field is not for the shell. It makes the model compress the action's intent into one stable natural-language sentence before generating command. It is a mini ReAct thought at the tool-call level — but one better suited to being logged, audited and approved by the runtime.
Tool output
Don't return only:
{"exit_code": 1}
A better return is structured state plus a natural-language summary:
{
"status": "failed",
"exit_code": 1,
"summary": "部署失败:3000 端口已被 nginx(pid 8432) 占用。",
"suggested_next_steps": ["改用其他端口", "确认后停止占用进程"]
}
The runtime can still rely on status / exit_code for deterministic control, while the model gets a high-probability path for its next action straight from summary and suggested_next_steps.
The pitfall in the bash echo pattern
cmd && echo "success" || echo "failed" is friendly to the model but not necessarily to the runtime: the last command executed is echo, so the final exit code of the whole shell command may become 0, masking the original command's failure.
The sturdier approach is for the tool wrapper to capture the original exit code and return, together:
- the original
stdout/stderr - the original
exit_code - a normalized
status - a model-facing natural-language
summary - optional
suggested_next_steps
In other words: don't use echo to stand in for a state machine; let the wrapper translate machine state into state both humans and models can read.
Article angles worth developing
- Title direction:
Why agent tool output shouldn't return only exit_code - Central question: is a tool result machine state, or context for the model to keep generating from?
- Key contrast: bare status codes, natural-language summaries, structured diagnosis — who does each serve?
- Connects to existing threads: tool call role design, Trace Log, multi-agent veto boundary
- Closing point: a good tool result should be simultaneously judgeable by a program, auditable by a human, and actionable by a model
References
- GNU Bash Manual, Comments
- Wei et al., Chain-of-Thought Prompting Elicits Reasoning in Large Language Models
- Yao et al., ReAct: Synergizing Reasoning and Acting in Language Models
- Anthropic Claude Docs, Define tools
- Anthropic Claude Docs, Handle tool calls
- Anthropic Claude Docs, Bash tool
- OpenAI API Docs, Function calling
- Claude Code Docs, Hooks reference
- OpenCode, Tools docs
- OpenCode source checked locally from
opencode-ai/opencodecommit73ee493, especiallyinternal/llm/tools/bash.goandinternal/llm/prompt/coder.go