Deconstructing CodeSmith's Internal Mechanisms: How AI Tools Constrain Model Behavior

How AI tool descriptions act as "house rules" shaping model behavior via instructions, errors, and engine checks.
This article dissects the underlying logic of CodeSmith's tool design: tool descriptions are written first for the model, where "when to use" matters far more than "what it can do." It covers three layers—instruction-based guidance (ten "Use this instead of" anti-pattern lists plus byte-stable call examples boosting accuracy from ~72% to 90%), error-message correction (edit_file's multi-match errors and three-level fuzzy fallback), and engine-level interception (pagination caps, count-then-truncate, fetch_url's seven SSRF gates with DNS pinning). It concludes that these house rules guard against the model's own clumsiness, while prompt injection is an adversarial threat of a different order.
When Tool Descriptions Become the Model's "House Rules"
Most developers write tool descriptions with the question "what does this tool do" in mind. But CodeSmith's approach reveals a completely different philosophy: tool descriptions are written first and foremost for the model to read — "when to use me, when not to use something else" matters far more than "what I can do."
In crates/tui/src/tools/file.rs:32, the read_file description reads:
Read a UTF-8 file from the workspace. Use this instead of
cat,head,tail, orsed -n '..p'inexec_shell— it's faster, sandbox-aware, and skips the approval prompt.
This passage deserves close reading. "Use me, not cat/head/tail/sed" — scattered throughout the repository's tool descriptions are ten such "Use this instead of ..." anti-pattern lists. write_file discourages heredoc, edit_file discourages sed -i, fetch_url discourages curl, grep_files discourages grep -r.

These sentences are not documentation written for humans — they are internal rules written for the model. Models have a natural preference for shell (cat and curl are ubiquitous in their training data), and shell happens to be the most expensive, most dangerous, hardest-to-approve path. Writing "don't use that, use this" into tool descriptions is the lowest-cost behavior-shaping technique available — the instructions and tool catalog sit in front of the model on every request.
This set of house rules operates on three levels: instruction-based guidance, error-message correction, and engine-level interception.
How Instructions Teach the Model
Beyond the anti-pattern lists, eight high-frequency tools also append call examples at the end of their descriptions. read_file's description closes with a static string:
Examples: {"path": "src/main.rs"} (first 200 lines);
{"path": "src/big.rs", "start_line": 201, "max_lines": 200}
These examples are byte-stable static text — no timestamps, no request IDs — so all prefix caching remains unaffected. And a model that copies the parameter shape from an example skips an entire round of "parameter error → error report → retry."
This investment in examples has experimental backing: attaching 1-5 real call examples to tool descriptions has raised tool-calling accuracy on certain benchmarks from roughly 72% to 90%. JSON Schema can express types, but it cannot convey implicit conventions like "is the timestamp in seconds or milliseconds" or "how are filter conditions nested" — examples are the most direct way to communicate them.
More fundamental than examples is the orientation of the description: "when to use" matters more than "what it can do." "Search for relevant content" is far inferior to "use when you need real-time information or to look up unknown facts" — the former describes functionality, the latter helps the model make a call decision. And clear boundary counterexamples (the ten "Use this instead of" lines from the opening) are often more important than capability lists, because the root cause of most failed calls is not that the model doesn't know what a tool can do, but that it doesn't know what the tool cannot do.
A debugging principle follows: when an Agent keeps choosing the wrong tool, fix the description first, suspect the model second. The return on editing a description far exceeds upgrading to a stronger model.
Prefix Caching is a cost optimization technique in large model inference: when multiple requests share the same prefix (e.g., a fixed system prompt, tool definitions, instruction text), the server can reuse already-computed key-value (KV) cache and only recompute the changing suffix. This significantly reduces latency and token costs. The reason tool description examples must be "byte-stable static text" is precisely that any subtle change — an extra timestamp, an inserted request ID — invalidates the cache hit for the entire prefix, causing you to pay for a brand-new prefix every time. Therefore, anything spliced into tool descriptions should be treated as part of the cache key, kept literally constant.
File Reading: One Page at a Time as a Compatibility Design
read_file's pagination constants are designed with considerable restraint: 200 lines by default, a hard cap of 500 lines, and 16KB of visible bytes. Small files (≤200 lines and ≤16KB, no explicit range) take a fast path and are returned as-is without wrapping — comments explicitly state this is to preserve historical behavior and avoid existing sessions suddenly seeing a new wrapper format.
The line-number format for large-file output is {line_no:>6}│ {line}, six columns right-aligned plus a full-width vertical bar — essentially a modernized cat -n.
Two design details are worth noting. Paging past the end is not an error: when start_line exceeds the total line count, return the [NO CONTENT] sentinel instead of throwing — if it flipped past the last page, tell it the book is finished, don't punish it.
The truncation notice hands over the parameters for the next call directly: To continue, call read_file with path="..." start_line=N max_lines=M — the model can copy it verbatim to continue reading, no need to mentally compute line numbers.
File Editing: The Engineering of Exact Matching
edit_file is a textbook case of exact-match replacement. The validation chain stacks layer upon layer: search == replace is rejected outright; replace_all and occurrence are mutually exclusive; occurrence must be a 1-based positive integer.
Then comes the most central rule — multiple matches means failure, and the failure message is itself an instruction:
Search string matched {count} locations in {path}; pass replace_all=true to replace every occurrence, or occurrence=N (1..={count}) to replace a specific one
It tells the model how many matches there were, the two ways out, and the valid range for N. A model receiving this error will naturally get it right on the next call. A good error message is not a status report — it's a blueprint for the next step.
Three Levels of Fuzzy Fallback
After exact matching fails, a three-level fuzzy fallback kicks in, each level targeting a real failure mode:
Level one tolerates leading whitespace: even when indentation is misaligned (spaces and tabs that look identical to the eye), it can still locate the match, using a byte-mapping table to map normalized positions back to the original span.
Level two normalizes typographic punctuation: curly quotes → straight quotes, em dashes → --, non-breaking spaces → regular spaces — comments state outright this is to catch paste failures where "browsers/chat clients silently substitute punctuation." When a model copies a code snippet from a web page, the punctuation has already been corrupted.
Level three strips line-number prefixes: when the model copies the {n:>6}│ line-number prefix from read_file output verbatim into old_string — the fallback chain strips the prefix before matching, recognizing its own format as well as cat -n/grep -n variants.
The guardrails are two-layered: multi-line text requires every line to carry the prefix with strictly increasing line numbers; single-line text enables a line-number alignment check — the stripped text must land exactly on the line indicated by the first line number.
One subtle detail: both the two fuzzy fallback levels and the scanner cursor of the line-number matcher advance by "skip the entire match," never "+1 byte" — a multi-byte leading character can never cut the next slice into the middle of a character, which is exactly the boundary guarded by CJK regression tests.
An Industry Lesson
The existence of fuzzy fallback has an instructive industry story behind it. One version of Cursor silently converted Chinese curly quotes to English straight quotes at the parameter-passing layer: the read tool returned curly quotes as-is, while the replace tool's parameters were silently straightened at the entry point — so "what I clearly saw, the tool says it can't find," and the model retries, fails, retries again, fails again, with no way to diagnose.
This violates a bottom line of tool design: there must be no systematic bias between the world the model perceives and the world the tool operates on. Normalization is fine, but it must be written into the description and declared in the return value; otherwise "clever correction" creates a systematic failure the model cannot self-diagnose.
CodeSmith's normalization takes the right path: it doesn't change parameters, doesn't change files, only tolerates during matching — a search written with curly quotes can match the original text written with straight quotes, while the file itself is not altered by a single byte.
TOCTOU is short for Time-of-Check to Time-of-Use, referring to a class of race-condition vulnerabilities where a time window exists between "check time" and "use time" during which state may have changed. In cybersecurity, attackers often make the system see a safe value during the "check" phase, then flip the actual value during the "use" phase. DNS rebinding is a classic TOCTOU: the domain first resolves to a safe public IP (passing validation), then before the connection is established the attacker modifies DNS to point to an internal address (the actual connection). Defenses therefore cannot rely on a one-time validation alone — the "validated IP" must be pinned into the connector so that the connection target matches the validation target.
The Five Approaches to File Editing and Their Trade-offs
Zooming out, the design space for file editing contains five approaches, each with winners and losers:
Diff description + Apply Model: generate a diff and hand it to a specialized small model to merge — Cursor's old route. The division of labor is elegant, but when the description and the original text diverge, the merge step lands in the wrong place, and once base models got stronger this route was abandoned.
Old string → new string: the shared choice of Claude Code, Codex, and today's Cursor — predictable and transparent. The cost is that deleting a large block of content requires quoting the original verbatim, and a single character's drift causes a match failure.
Line-number positioning: "delete lines X through Y" — precise and token-efficient, but every edit renumbers everything after it; when a single thinking output contains multiple edits, they must all use the initial line numbers, and models easily get confused.
Vim-like commands: suited to refactoring code, but the learning burden of command syntax is a disaster for weaker models. Deeper still, Vim was designed for humans who "watch and edit in small steps," whereas a model's working mode is "think long, edit large blocks."
Head-and-tail anchor matching: reference only the first few and last few lines, omitting the middle — edit_file's search_start + search_end is exactly this approach's implementation, combining the reliability of content matching with the token savings of the line-number approach.
The five approaches face the same exam: between the fuzziness of human language and the precision of machine execution, where exactly should the interface be cut?
Search Tools: Count Everything First, Then Truncate
Search tools share a gentle design in their truncation strategy. grep_files caps results at 100, but its return structure carries "truncated": total_matches > max_results — count all matches first, then truncate the display. "347 total hits, here are the first 100" and "here are 100 hits" are two completely different bases for decision-making for a model.
The excluded-directory list is written as double insurance (each directory listed with both spellings): node_modules, .git, target, *.min.js, dist, build, __pycache__, .venv. Files over 10MB are skipped outright.
Search tools themselves come in four categories:
- Regex content matching (grep/ripgrep) — fastest and most accurate when you know the literal text to find
- Filename patterns (glob) — the first step in exploring project structure
- Semantic search — used when code "means something similar but is worded differently"
- Symbol-level lookup — IDE-style "go to definition / find references," which plain-text search cannot distinguish
Whether to build an embedding index for semantic search once sparked a school-of-thought battle in the industry: terminal Agents like Claude Code deliberately refuse it, relying on pure grep + glob for on-the-spot retrieval, so there's no index that needs to stay fresh as code evolves, saving an entire index infrastructure; Cursor took the opposite route early on, then also retreated to grep + glob.
CodeSmith stands on the no-index side — the model's search tool surface is precisely grep + glob.
Network Policy: Three-State Configuration and Deny-First
The network is another country. Policy configuration has three states: Allow | Deny | Prompt, and two design decisions are written directly into comments.
A misconfigured value does not equal implicit allow: all unknown values fall back to Prompt — "a single typo must never silently disable a security policy." Deny has veto power: hosts matching both allow and deny are treated as deny.
Domain matching has a counterintuitive detail: .example.com matches any subdomain but does not match the bare domain — if you want both, list both.
The default sits on the safe side: when the [network] config section is missing, config loading returns a decider with default = "prompt" — the first time a network tool touches an unapproved host, it pops the standard inline approval, never a silent pass.
The semantics of Prompt got a new lease on life. The old implementation converted Prompt into permission_denied, and the error even instructed users to "rerun with /network allow <host>" — but that was a dead end: follow the instructions, write the config, and the live engine still never refreshes its policy snapshot, so retries fail just the same. The new mapping hands "ask" over to the inline approval gate at dispatch time.
SSRF (Server-Side Request Forgery) refers to an attacker inducing a server-side program to make requests to unintended addresses, commonly used to probe internal networks, access cloud metadata services, or bypass firewalls. In Agent tool scenarios, a fetch_url target that the model is induced into or inadvertently constructs may point to 127.0.0.1, internal RFC1918 addresses, or a cloud provider's metadata endpoint (such as 169.254.169.254), thereby leaking sensitive configuration or credentials. This is why fetch_url needs a protocol allowlist, a restricted-IP table, DNS pinning, and per-hop redirect re-validation working in concert — "just disable localhost" alone is not enough to cover the full attack surface.
fetch_url: Seven Gates for One URL
fetch_url turns SSRF defense into a pipeline, in execution order:
- Protocol allowlist: only http/https accepted, with a dedicated test case for
file:///etc/passwd - Policy evaluation: passes through the three-state table above
- localhost hostname rejection
- Literal IP check: including bracketed IPv6
- Restricted-IP table: loopback, RFC1918 private networks, link-local, multicast, CGNAT, cloud metadata
169.254.169.254, IETF benchmark ranges, Class E addresses - Per-IP DNS validation + pinning: every resolved address passes the restricted table, then the first legitimate IP is pinned into the connector — this defends against "DNS rebinding (TOCTOU)": at validation time the domain resolves to a safe IP, but at connection time re-resolution may be flipped inward
- Per-hop redirect re-validation: follow redirects manually (cap of 5 hops), with every hop re-running all the gates — a safe first hop does not guarantee a safe third hop
Another behavioral detail: 4xx/5xx are not tool errors — return the body and success: false. An API's error response body is usually a large block of JSON, itself a diagnostic clue, and it's a shame to discard it.
The Three Lines of Defense of the House Rules
Let's gather the house rules into three layers. Instruction-based guidance: ten "Use this instead of" lines pull the model back from shell to structured tools, saving approvals, context, and risk at the source. Error-message correction: multi-match errors carry parameters, hunk failures carry previews, network blocks carry reasons — every error is a blueprint for the next step. Engine-level interception: pagination caps, count-then-truncate, three-segment path checks, the seven SSRF gates, DNS pinning — the model would find it hard to make a mistake even if it tried.
The house rules dissected in this article guard against the model's clumsiness: trembling hands, a preference for shell, flipping one page too many. But mixed into the context is something else entirely — malicious content carefully planted by others. A line in a web page saying "ignore the previous instructions," a command disguised as a project convention in AGENTS.md: that is an opponent of a completely different order.
Background
Prompt Injection refers to an attacker hiding malicious instructions in content the model will read — web page bodies, files, tool return values, even AGENTS.md text disguised as project configuration — inducing the model to deviate from its original task, leak information, or perform dangerous operations. It is fundamentally different in nature from what the "house rules" described in this article guard against: house rules address the model's own clumsiness and preferences (misusing shell, paging past the end), an engineering problem fixable through descriptions and validation; prompt injection, by contrast, is adversarial input carefully designed by an external opponent, requiring trust-boundary demarcation, isolation of content from instructions, and treating tool results as data rather than commands. The two defense philosophies must not be conflated.
One-line Summary
Revealing how AI tool descriptions serve as "house rules" that shape model behavior through instructions, error messages, and engine-level interception.
Related articles

WorkBuddy Step-by-Step Guide: Build Your Own AI Agent Work Assistant
WorkBuddy step-by-step guide: starting from the limits of chat models like Doubao, learn to build your own AI Agent work assistant for batch local file processing and automation—no programming required.

Building a Local Knowledge Base with LangChain-Chatchat + DeepSeek on Windows
This article, based on a Bilibili tutorial, details the full workflow of building a local knowledge base on Windows using LangChain-Chatchat and DeepSeek, including Anaconda environment setup, Ollama model management, mirror acceleration, and common error solutions, helping you quickly implement RAG Q&A.

Bilibili DeepAgents Practical Tutorial: From LangChain Basics to Agent Development
Bilibili DeepAgents tutorial practical analysis: from LangChain framework basics to agent development, covering the core path of building large model applications, including code implementation and learning advice, suitable for AI application development beginners.