The Hook signatures are identical to Yao Agent. What changes is how you use them:
- **Create Hook** — write context files before Claude Code starts
- **Next Hook** — read files Claude Code produced, then surface the result
## Create Hook: Prepare Context
Claude Code reads from the workspace. Write everything Claude needs to understand the task before returning from Create Hook.
```typescript
// assistants/my-agent/src/index.ts
import { agent, Process } from "@yao/runtime";
export function Create(
ctx: agent.Context,
messages: agent.Message[]
): agent.Create {
const input = messages[messages.length - 1]?.content || "";
// Write a structured context file for Claude Code to read
ctx.workspace.WriteFile("context.md", `
# Task
${input}
# Instructions
- Output your implementation to \`output/\`
- Write a summary to \`output/summary.md\`
- Do not modify files outside of \`output/\`
`);
// Optionally store timing for Next Hook
ctx.memory.context.Set("start_time", Date.now());
ctx.memory.context.Set("input", input);
return { messages };
}
```
### Injecting Dynamic Data
Load data from the database or other sources and include it in the context file:
```typescript
export function Create(
ctx: agent.Context,
messages: agent.Message[]
): agent.Create {
const input = messages[messages.length - 1]?.content || "";
const projectId = ctx.route?.match(/[?&]project=([^&]+)/)?.[1];
let projectContext = "";
if (projectId) {
const project = Process("models.project.Find", Number(projectId), {});
if (project) {
projectContext = `
# Project Context
Name: ${project.name}
Stack: ${project.stack}
Description: ${project.description}
`;
}
}
ctx.workspace.WriteFile("context.md", `
# Task
${input}
${projectContext}
# Instructions
Output implementation to \`output/\`.
`);
ctx.memory.context.Set("project_id", projectId || "");
return { messages };
}
```
### Running Setup Commands
Use `ctx.computer.Exec` to prepare the environment before Claude starts:
```typescript
export function Create(
ctx: agent.Context,
messages: agent.Message[]
): agent.Create {
// Ensure the output directory is clean
const exists = ctx.workspace.Exists("output");
if (exists) {
ctx.workspace.RemoveAll("output");
}
ctx.workspace.MkdirAll("output");
// Check current git state
const gitStatus = ctx.computer.Exec("git status --short");
ctx.workspace.WriteFile("context.md", `
# Task
${messages[messages.length - 1]?.content || ""}
# Current State
\`\`\`
${gitStatus.stdout}
\`\`\`
`);
return { messages };
}
```
---
## Next Hook: Collect Output
`payload.completion.content` in a CLI Agent contains the **text output Claude Code produced** — the concatenation of all text blocks streamed during execution.
To collect structured results, read files Claude Code wrote to the workspace:
```typescript
import { agent, Process } from "@yao/runtime";
export function Next(
ctx: agent.Context,
payload: agent.Payload
): agent.Next | null {
if (payload.error) {
ctx.Send({ type: "error", props: { message: payload.error } });
return { data: { status: "error" } };
}
// Read files Claude Code produced
if (!ctx.workspace.Exists("output/summary.md")) {
ctx.Send("Claude Code did not produce output. Please try again.");
return { data: { status: "no_output" } };
}
const summary = ctx.workspace.ReadFile("output/summary.md");
const files = ctx.workspace.ReadDir("output/");
// List generated files
const fileList = files
.filter((f) => !f.is_dir && f.name !== "summary.md")
.map((f) => `- ${f.name} (${f.size} bytes)`)
.join("\n");
const duration = Date.now() - (ctx.memory.context.Get("start_time") as number);
ctx.Send(`Done in ${duration}ms\n\n**Summary:**\n${summary}\n\n**Files:**\n${fileList}`);
return { data: { status: "success" } };
}
```
### Reading Structured Output
If Claude Code writes JSON:
```typescript
export function Next(
ctx: agent.Context,
payload: agent.Payload
): agent.Next | null {
if (payload.error) {
return { data: { status: "error", message: payload.error } };
}
let result: any;
try {
const raw = ctx.workspace.ReadFile("output/result.json");
result = JSON.parse(raw);
} catch {
ctx.Send("Failed to read result. Please try again.");
return { data: { status: "parse_error" } };
}
// Save to database
const id = Process("models.result.Save", result) as number;
// Navigate to result page
ctx.Send({
type: "action",
props: {
name: "navigate",
payload: { route: `/agents/my-agent/detail?id=${id}` },
},
});
return { data: { status: "success", id } };
}
```
---
## Payload Differences vs Yao Agent
| Field | Yao Agent | CLI Agent |
|-------|-----------|-----------|
| `payload.completion.content` | LLM's chat response text | Claude Code's text output (concatenated text blocks) |
| `payload.completion.tool_calls` | LLM tool calls | Not used (Claude Code handles tools internally) |
| `payload.tools` | Tool call results from Yao | Not populated |
| `payload.error` | LLM or tool error | Runner error (container crash, timeout, etc.) |
**The key shift:** in Yao Agent, `payload.completion.content` is the answer. In CLI Agent, the answer is usually in **files Claude Code wrote** — `payload.completion.content` is supplementary.
## What's Next
You know how Hooks work with CLI Agent. Build a complete end-to-end agent.
→ **[Build: Coding Agent](./build-coding-agent)**