Step-by-step checklists for common development tasks in the Claude Code codebase.
src/tools/MyTool/
Directory name must be PascalCase matching the tool name.
// src/tools/MyTool/prompt.ts
export const MY_TOOL_NAME = 'MyTool'
export function getDescription(): string {
return `Description of what the tool does.
Usage:
- When to use this tool
- Key capabilities
- Important constraints
`
}// src/tools/MyTool/MyTool.ts
import { z } from 'zod/v4'
import type { ValidationResult } from '../../Tool.js'
import { buildTool, type ToolDef } from '../../Tool.js'
import { getCwd } from '../../utils/cwd.js'
import { lazySchema } from '../../utils/lazySchema.js'
import { MY_TOOL_NAME, getDescription } from './prompt.js'
import {
getToolUseSummary,
renderToolResultMessage,
renderToolUseErrorMessage,
renderToolUseMessage,
} from './UI.js'
const inputSchema = lazySchema(() =>
z.strictObject({
param1: z.string().describe('Description of param1'),
param2: z.string().optional().describe('Optional parameter'),
}),
)
type InputSchema = ReturnType<typeof inputSchema>
const outputSchema = lazySchema(() =>
z.object({
result: z.string(),
count: z.number(),
}),
)
type OutputSchema = ReturnType<typeof outputSchema>
type Output = z.infer<OutputSchema>
export const MyTool = buildTool({
name: MY_TOOL_NAME,
searchHint: 'brief capability phrase for search',
maxResultSizeChars: 20_000,
strict: true,
async description() {
return getDescription()
},
userFacingName() {
return 'My Tool'
},
getToolUseSummary,
getActivityDescription(input) {
const summary = getToolUseSummary(input)
return summary ? `Running ${summary}` : 'Running tool'
},
get inputSchema(): InputSchema {
return inputSchema()
},
get outputSchema(): OutputSchema {
return outputSchema()
},
isConcurrencySafe() {
return true // Can it run in parallel with other tools?
},
isReadOnly() {
return true // Does it modify the filesystem or state?
},
async prompt() {
return getDescription()
},
renderToolUseMessage,
renderToolUseErrorMessage,
renderToolResultMessage,
async call(
{ param1, param2 },
{ abortController, getAppState },
) {
// Main tool logic
const result = await doWork(param1, param2)
return {
data: {
result: result.text,
count: result.count,
},
}
},
} satisfies ToolDef<InputSchema, Output>)// src/tools/MyTool/UI.tsx
import type { ToolResultBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
import React from 'react'
import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js'
import { MessageResponse } from '../../components/MessageResponse.js'
import { TOOL_SUMMARY_MAX_LENGTH } from '../../constants/toolLimits.js'
import { Text } from '../../ink.js'
import { truncate } from '../../utils/format.js'
type Output = {
result: string
count: number
}
export function getToolUseSummary(
input: Partial<{ param1: string }> | undefined,
): string | null {
if (!input?.param1) return null
return truncate(input.param1, TOOL_SUMMARY_MAX_LENGTH)
}
export function renderToolUseMessage(
{ param1 }: Partial<{ param1: string }>,
{ verbose }: { verbose: boolean },
): React.ReactNode {
if (!param1) return null
return `param1: "${param1}"`
}
export function renderToolUseErrorMessage(
result: ToolResultBlockParam['content'],
{ verbose }: { verbose: boolean },
): React.ReactNode {
return <FallbackToolUseErrorMessage result={result} verbose={verbose} />
}
export function renderToolResultMessage(
{ result, count }: Output,
_progressMessages: unknown[],
{ verbose }: { verbose: boolean },
): React.ReactNode {
return (
<MessageResponse>
<Text>
Found <Text bold>{count}</Text> results
</Text>
</MessageResponse>
)
}// Add import
import { MyTool } from './tools/MyTool/MyTool.js'
// Add to getAllBaseTools()
export function getAllBaseTools(): Tool[] {
return [
// ... existing tools
MyTool,
]
}- Directory:
src/tools/MyTool/(PascalCase) -
prompt.ts:MY_TOOL_NAMEconstant +getDescription()function -
MyTool.ts:buildTool({ ... }) satisfies ToolDef<InputSchema, Output> -
UI.tsx:getToolUseSummary,renderToolUseMessage,renderToolResultMessage,renderToolUseErrorMessage - Registered in
src/tools.ts - Input schema uses
lazySchema(() => z.strictObject({ ... })) - All relative imports use
.jsextension - Type-only imports use
import type - No semicolons, single quotes, trailing commas
src/commands/my-command/
Directory name must be kebab-case.
For a local command (my-command.ts):
// src/commands/my-command/my-command.ts
import type { LocalCommandCall } from '../../types/command.js'
export const call: LocalCommandCall = async (args, context) => {
const { abortController, messages } = context
const trimmedArgs = args.trim()
// Command logic here
return {
type: 'text',
value: `Result: ${trimmedArgs}`,
}
}For a JSX command (my-command.tsx):
// src/commands/my-command/my-command.tsx
import * as React from 'react'
import type { LocalJSXCommandCall } from '../../types/command.js'
import { MyComponent } from '../../components/MyComponent.js'
export const call: LocalJSXCommandCall = async (onDone, context, args) => {
return <MyComponent args={args} onClose={onDone} />
}// src/commands/my-command/index.ts
import type { Command } from '../../commands.js'
const myCommand = {
type: 'local', // or 'local-jsx'
name: 'my-command',
description: 'What this command does',
supportsNonInteractive: false,
load: () => import('./my-command.js'),
} satisfies Command
export default myCommand// Add import
import myCommand from './commands/my-command/index.js'
// Add to COMMANDS()
export const COMMANDS = memoize(() => [
// ... existing commands
myCommand,
])- Directory:
src/commands/my-command/(kebab-case) - Implementation:
my-command.tsormy-command.tsx - Registration:
index.tswithsatisfies Commandandexport default - Registered in
src/commands.tsCOMMANDS array -
load: () => import('./my-command.js')for lazy loading - All relative imports use
.jsextension - No semicolons, single quotes, trailing commas
| Utility Type | File |
|---|---|
| Error handling | src/utils/errors.ts |
| File operations | src/utils/file.ts |
| Path manipulation | src/utils/path.ts |
| String operations | src/utils/stringUtils.ts |
| Environment checks | src/utils/envUtils.ts |
| Format/display | src/utils/format.ts |
| New domain | src/utils/myNewUtil.ts (camelCase) |
/**
* Brief description of what the function does.
* Explain when to use it and any important caveats.
*/
export function myUtilFunction(param: string, options?: { flag?: boolean }): string {
if (!param) {
return ''
}
// Implementation
return result
}- JSDoc comment with purpose and usage context
- Exported with
export function(named export) - Return type annotated explicitly
- Guard clauses for edge cases
- No semicolons, single quotes, trailing commas
- If stateful, provide
_resetForTesting()helper
| Type Category | File |
|---|---|
| Pure types (no runtime deps) | src/types/myTypes.ts |
| Types tied to a module | Same file as the module |
| Complex schemas | Dedicated types.ts in tool/service directory |
// src/types/myTypes.ts
/**
* Description of what this type represents.
*/
export type MyNewType = {
/** Description of this field */
id: string
/** Description of this field */
status: 'active' | 'inactive'
/** Optional field with default behavior */
metadata?: Record<string, unknown>
}- JSDoc on the type and non-obvious properties
- Use
type(notinterface) — the codebase preferstype - String literal unions (not
enum) for status/mode fields -
as constarrays when runtime validation of values is needed - No runtime dependencies in
src/types/files -
export typewhen re-exporting
Before submitting any change, verify:
-
.jsextension on all relative imports -
import typefor type-only imports - Cherry-picked lodash (
import memoize from 'lodash-es/memoize.js') - Zod from
'zod/v4'
-
camelCasefor functions and variables -
PascalCasefor types, classes, components -
UPPER_SNAKE_CASEfor constants -
_prefix for unused parameters
- No semicolons
- Single quotes
- Trailing commas
- 2-space indentation
-
toError()/errorMessage()at catch boundaries (not raw casts) -
isENOENT()/isFsInaccessible()for fs errors (not(e as ...).code) -
logError()for error logging - Graceful fallbacks, not crashes
- JSDoc on exported APIs
- Comments explain why, not what
- Lint suppressions include explanations
- Silent catches have explanatory comments
- No
any(useunknownand narrow) - Branded types for IDs
-
satisfieson tool/command definitions -
as conston literal arrays