docs: Added common pitfalls docs for custom bash scripts

2026-07-14 12:46:17 -06:00
parent b88c6bc579
commit 69a57d8872
+111
@@ -336,6 +336,117 @@ argc_some_field="$(jq -r '.some_field' <<< "$LLM_AGENT_RAW_JSON")"
---
# Output Handling and Common Pitfalls
Coyote captures your tool's result from the `$LLM_OUTPUT` file (not from stdout), and it applies a few rules that are
easy to trip over. The bundled `fs_*` tools follow the patterns below; your own tools should too.
## An empty result is shown to the model as `"DONE"`
When a tool exits `0` but writes **nothing** to `$LLM_OUTPUT`, Coyote has no content to return, so the model receives
the bare string `"DONE"`. The model cannot distinguish that from a broken tool. It has no idea whether the file was
empty, the search had no hits, or the script silently failed.
Always write *something* for every successful outcome, including the "nothing found" cases:
```bash
# BAD: a search that finds nothing writes nothing, so the model just sees "DONE"
main() {
grep -rn "$argc_pattern" . >> "$LLM_OUTPUT"
}
# GOOD: empty results get an explicit, informative message
main() {
local results
results=$(grep -rn "$argc_pattern" . 2>/dev/null) || true
if [[ -z "$results" ]]; then
echo "No matches found for: $argc_pattern" >> "$LLM_OUTPUT"
return 0
fi
echo "$results" >> "$LLM_OUTPUT"
}
```
The same applies to reading an empty file, listing an empty directory, or an offset/range that lands past the end of
the data. If a "no result" is a normal outcome, say so explicitly.
## Errors written to `$LLM_OUTPUT` are discarded on a non-zero exit
Coyote reads `$LLM_OUTPUT` **only when the tool exits `0`**. On a non-zero exit it instead returns
`Tool call '<name>' exited with code N` plus whatever the tool printed to **stdout/stderr**. The `$LLM_OUTPUT` file is
ignored. So the following hides your helpful message; the model only sees `exited with code 1`:
```bash
# BAD: the message lands in $LLM_OUTPUT, then the non-zero exit makes Coyote ignore it
main() {
if [[ ! -d "$argc_path" ]]; then
echo "Error: directory not found: $argc_path" >> "$LLM_OUTPUT"
return 1
fi
}
```
Use one of these instead:
```bash
# OPTION A (real errors): print to stderr, then exit non-zero.
# Coyote captures stderr and attaches it to the error the model sees.
main() {
if [[ ! -d "$argc_path" ]]; then
echo "Error: directory not found: $argc_path" >&2
exit 1
fi
}
# OPTION B (expected "empty"/"not found" outcomes that are not real failures):
# write the note to $LLM_OUTPUT and exit 0 so Coyote returns it as normal content.
main() {
if [[ ! -d "$argc_path" ]]; then
echo "No such directory: $argc_path" >> "$LLM_OUTPUT"
return 0
fi
}
```
## Preserve a file's final line when it has no trailing newline
Two common idioms silently drop the last line of a file that does not end in `\n`:
- `while IFS= read -r line; do ...; done` stops before the final unterminated line, because `read` returns non-zero on it.
- `wc -l` counts *newlines*, so it under-reports the line count by one for such files.
```bash
# BAD: drops the final line when the file ends without a newline; total is off by one
total=$(wc -l < "$file")
while IFS= read -r line; do
printf '%s\n' "$line"
done < "$file"
# GOOD: `|| [[ -n "$line" ]]` processes the final unterminated line; awk counts it correctly
total=$(awk 'END { print NR }' "$file")
while IFS= read -r line || [[ -n "$line" ]]; do
printf '%s\n' "$line"
done < "$file"
```
## Type numeric options with `<INT>` / `<NUM>`
An `@option` with no value notation compiles to a **string** in the JSON schema, so the model tends to send `"359"`
instead of `359`. Add an `<INT>` (integer) or `<NUM>` (floating point) notation so the schema advertises the right type:
```bash
# BAD: schema type is "string"; the model sends "359"
# @option --offset The line to start from
# GOOD: schema type is "integer"; the model sends 359
# @option --offset <INT> The line to start from
```
Your script still receives the value as text in `argc_offset` either way (Bash has no typed variables), but the schema
now guides the model and lets strict providers validate the call.
---
# Prompt Helpers
It's often useful to create interactive prompts for our bash tools so that our tools can get input from
users.