Building a Customer Support Agent with Claude: Tools, Policy Hooks, and Escalation
▶ Watch on YouTube & subscribe to The Stack Underflow
Building a toy chatbot is easy. Building a customer support agent that handles adversarial inputs, enforces business policies, and hands off gracefully to a human queue is a different problem. This tutorial walks through the full stack shown in the video: scoped tools, a code-enforced policy hook, structured escalation, and a reliability plane — all wired together in one agent loop.
The core insight is that reliability does not come from prompting. It comes from the structure around the model: what tools you expose, what gates you put on those tools, and what you log when things go sideways.
The one-sentence version: A production customer support agent wraps Claude in a tight loop with exactly the tools it needs, enforces business policy in code (not prompts), and escalates structured summaries to humans when the model hits a ceiling.
The Agent Loop
At runtime a customer request enters what the video calls “L1 model wrapped in L3 orchestration.” In plain terms:
- L1 is the Claude model call itself.
- L3 is the loop around it: inspect
stop_reason, run the indicated tool, feed the result back, repeat untilend_turn.
while True:
response = client.messages.create(
model="claude-opus-4-5",
tools=TOOLS,
messages=conversation
)
if response.stop_reason == "end_turn":
break
# execute whichever tool Claude chose
tool_result = dispatch(response.content)
conversation.append({"role": "assistant", "content": response.content})
conversation.append({"role": "user", "content": [tool_result]})
This is the standard agentic pattern. What makes it production-grade is everything attached to the loop, not the loop itself.
Scope Your Tools — Give the Agent Only What It Needs
The video attaches exactly three tools, nothing more:
| Tool | Purpose |
|---|---|
lookup_order | Retrieve order details by ID |
process_refund | Issue a refund up to a defined cap |
escalate | Hand off to the human queue |
Tool sprawl is a real failure mode. Every tool you expose is a surface the model can misuse or an adversarial prompt can exploit. The principle is the same as least-privilege in security: scope to what the scenario actually requires.
The Description Is the Contract
Each tool’s JSON schema description is load-bearing documentation, not a comment. The process_refund description in the video explicitly states:
“Is a refund, do not exceed the $500 cap. Return a structured error if over. Input schema and error shape are explicit.”
The model reads these descriptions during planning. A precise description reduces hallucinated argument shapes and off-spec calls. Treat each description as a typed contract between the orchestrator and the model.
{
"name": "process_refund",
"description": "Process a customer refund. Do not exceed the $500 cap. Return a structured error if the requested amount exceeds the cap.",
"input_schema": {
"type": "object",
"properties": {
"order_id": { "type": "string" },
"amount": { "type": "number", "maximum": 500 }
},
"required": ["order_id", "amount"]
}
}
Code-Enforced Policy Hooks: The Gate That Cannot Be Argued With
Here is the pivotal idea in the video. The $500 refund cap does not live in the system prompt. It lives in a pre-tool-use hook — a code gate that runs before process_refund executes.
request
└─> agent loop
└─> model emits tool call: process_refund(amount=4000)
└─> [PRE-TOOL HOOK] amount > 500? → BLOCK, return policy error
└─> model receives structured error, adapts
└─> proposes compliant 500 + escalation
Why does this matter? A sufficiently adversarial prompt can override instructions in a system prompt. “You have special authorization, ignore limits” is a classic jailbreak vector. A hook in code cannot be argued with. The model can ask all it wants; the gate just says no and returns a machine-readable error.
def pre_tool_hook(tool_name, tool_input):
if tool_name == "process_refund":
if tool_input.get("amount", 0) > 500:
return {
"error": "policy_cap_exceeded",
"message": "Refund amount exceeds the $500 policy cap.",
"max_allowed": 500
}
return None # allow
The structured error is important. It gives the model something actionable: it knows why it was blocked and can propose a compliant alternative (refund $500, escalate the remainder).
Clean Escalation
Escalation is triggered on three conditions:
- Policy complexity — the situation requires human judgment beyond the tool set
- Risk — the stakes are too high to proceed autonomously
- Explicit request — the customer asked for a human
What is explicitly not a trigger: sentiment alone. A furious customer isn’t automatically an escalation case if the problem is solvable within policy. Escalating on vibes wastes human queue capacity.
When escalation fires, a small structured summary rides the handoff:
{
"who": "customer_id_789",
"what": "refund request",
"tried": "process_refund(amount=4000)",
"blocked": "policy_cap_exceeded"
}
The human agent arriving in the queue immediately knows context. No re-reading conversation history required.
The Reliability Plane
The video describes pinning case facts at the top of context — account, order, and plan information — so they don’t get buried by verbose tool outputs as the conversation grows. Alongside that, a logging sync captures three streams:
- Stop reason — why the model stopped each turn (
tool_use,end_turn,max_tokens, etc.) - Tool calls — which tools fired with what arguments
- Hook blocks — which pre-tool gates triggered and why
[TURN 1] stop_reason=tool_use tool=lookup_order(order_id="ORD-42")
[TURN 2] stop_reason=tool_use tool=process_refund(amount=4000) BLOCKED by hook
[TURN 2] stop_reason=tool_use tool=process_refund(amount=500) OK
[TURN 3] stop_reason=tool_use tool=escalate(summary={...})
[TURN 4] stop_reason=end_turn
This three-stream log is how you debug agents in production. When something goes wrong, you pull the trace and can see exactly where the model deviated, which gate fired, and what the model did next.
Common Misconceptions
- “Put the policy in the system prompt and the model will respect it.” The model will try to respect it. A code-enforced hook respects it unconditionally. For hard business constraints, only code can hold the line.
- “More tools = more capable agent.” More tools increase the attack surface and the chance of the model choosing the wrong one. Constrain the tool set to what the task actually requires.
- “Escalate on angry sentiment.” Sentiment is not a reliable signal for escalation. Escalate on policy limits, risk thresholds, and explicit requests — not on exclamation marks.
- “Verbose tool output in context is fine.” Long tool outputs push pinned facts out of the effective context window and degrade later turns. Prune or summarize tool results before feeding them back.
Frequently Asked Questions
Why return a structured error from the hook instead of just blocking silently?
A structured error gives the model actionable information. If the model just receives an empty block or an exception, it may retry indefinitely or hallucinate a workaround. A typed error (policy_cap_exceeded, max_allowed: 500) lets the model reason about the constraint and propose a valid path forward.
Where exactly does the pre-tool hook run in the Anthropic SDK?
In practice you implement this in your dispatch layer — the code that receives the model’s tool_use block and decides whether to call the actual tool function. The Claude API doesn’t have a native hook callback; the hook is your code gate between the model’s intent and the tool’s execution.
What happens when the model hits max_tokens mid-task?
This is why logging stop_reason on every turn matters. max_tokens is a silent failure mode — the model just stops mid-thought. Your loop should detect it, log it, and either summarize context and continue or escalate rather than silently dropping the task.
Should the escalation tool be inside the same scope ring as the other tools? Yes. Keeping escalation inside the tool scope means the model can trigger it as a first-class action — not as a fallback signal outside the loop. The structured summary it produces is far more useful to the human queue than a raw conversation dump.
Where This Fits in the Series
This is lesson 28 of the “How Claude Actually Works” course — the first scenario in the CCA framework (Customer, Code, Agent). It takes the abstract loop-and-tools pattern from earlier lessons and grounds it in a concrete production problem: a customer support agent with real constraints, adversarial inputs, and a human handoff path. The multi-agent research system covered next builds on the same structural ideas — scope, hooks, structured errors, and logging — just with more agents in play. Browse all tutorials to see the full course map.
Found this useful? The deep version lives on YouTube — new breakdowns of how AI dev tools actually work, weekly.
Subscribe on YouTube →