Formatting Rules
April 1, 2026 · View on GitHub
This document covers every formatting rule and code style convention used in the Claude Code codebase. The project uses Biome for formatting and ESLint + custom rules for linting.
No Semicolons
The codebase omits semicolons entirely. This is enforced by the formatter:
// CORRECT — no semicolons
import { getCwd } from '../../utils/cwd.js'
const result = await search()
export function toError(e: unknown): Error {
return e instanceof Error ? e : new Error(String(e))
}
// WRONG — has semicolons
import { getCwd } from '../../utils/cwd.js';
const result = await search();
Exception: Semicolons appear in compiled/generated UI files (React compiler output) — those files are auto-generated and should not be manually edited.
Single Quotes for Strings
Always use single quotes for string literals:
// CORRECT
const name = 'GrepTool'
import { getCwd } from '../../utils/cwd.js'
this.name = 'AbortError'
// WRONG
const name = "GrepTool"
import { getCwd } from "../../utils/cwd.js"
Exception: Template literals (backticks) when interpolation or multiline is needed:
const message = `Found ${count} files`
const description = `A powerful search tool built on ripgrep
Usage:
- ALWAYS use ${GREP_TOOL_NAME} for search tasks.
`
Trailing Commas
Use trailing commas in all multiline constructs:
Function Parameters
export class ShellError extends Error {
constructor(
public readonly stdout: string,
public readonly stderr: string,
public readonly code: number,
public readonly interrupted: boolean, // <-- trailing comma
) {
super('Shell command failed')
}
}
Object Literals
const LEVEL_ORDER: Record<DebugLogLevel, number> = {
verbose: 0,
debug: 1,
info: 2,
warn: 3,
error: 4, // <-- trailing comma
}
Array Literals
const VCS_DIRECTORIES_TO_EXCLUDE = [
'.git',
'.svn',
'.hg',
'.bzr',
'.jj',
'.sl', // <-- trailing comma
] as const
Function Arguments
const result = await checkReadPermissionForTool(
GrepTool,
input,
appState.toolPermissionContext, // <-- trailing comma
)
Import Lists
import {
type DebugFilter,
parseDebugFilter,
shouldShowDebugMessage, // <-- trailing comma
} from './debugFilter.js'
2-Space Indentation
The entire codebase uses 2 spaces for indentation:
export function logForDebugging(
message: string,
{ level }: { level: DebugLogLevel } = {
level: 'debug',
},
): void {
if (LEVEL_ORDER[level] < LEVEL_ORDER[getMinDebugLogLevel()]) {
return
}
if (!shouldLogDebugMessage(message)) {
return
}
const timestamp = new Date().toISOString()
const output = `${timestamp} [${level.toUpperCase()}] ${message.trim()}\n`
getDebugWriter().write(output)
}
No Explicit return undefined
Use bare return instead of return undefined:
// CORRECT
export function getErrnoCode(e: unknown): string | undefined {
if (e && typeof e === 'object' && 'code' in e) {
return e.code
}
return undefined // Exception: when the type signature requires it explicitly
}
// For void functions, just use return with no value
export function logError(error: unknown): void {
if (isDisabled) {
return
}
// ...
}
Template Literals for Interpolation
Always use template literals when embedding expressions:
// CORRECT
const message = `Path does not exist: ${path}`
const output = `${timestamp} [${level.toUpperCase()}] ${message.trim()}\n`
const result = `Found ${numFiles} ${plural(numFiles, 'file')}`
const logPath = `${getClaudeConfigHomeDir()}/debug/${getSessionId()}.txt`
// WRONG — string concatenation
const message = 'Path does not exist: ' + path
const output = timestamp + ' [' + level.toUpperCase() + '] ' + message + '\n'
Ternary Expressions
Simple Ternaries (Single Line)
const effectiveLimit = limit ?? DEFAULT_HEAD_LIMIT
const result = count === 1 ? word : `${word}s`
return e instanceof Error ? e : new Error(String(e))
Multiline Ternaries (for Complex Expressions)
const useFirstPrompt = strippedFirstPrompt && !isAutonomousPrompt
const title =
log.agentName ||
log.customTitle ||
log.summary ||
(useFirstPrompt ? strippedFirstPrompt : undefined) ||
defaultTitle ||
(isAutonomousPrompt ? 'Autonomous session' : undefined) ||
(log.sessionId ? log.sessionId.slice(0, 8) : '') ||
''
Ternaries in JSX
{count > 0 && <CtrlOToExpand />}
{secondaryCount !== undefined && secondaryLabel
? <Text>across <Text bold>{secondaryCount}</Text> {secondaryLabel}</Text>
: null}
Spread Operator for Array/Object Combination
// Combining arrays
const tools = [...builtInTools, ...mcpTools, ...pluginTools]
const modes = [...EXTERNAL_PERMISSION_MODES, ...conditionalModes]
// Conditional spreading
const allTools = [
GrepTool,
FileEditTool,
...(SleepTool ? [SleepTool] : []),
]
// Object spread for copies
return [...inMemoryErrorLog]
const queuedEvents = [...errorQueue]
// Conditional object properties
const output = {
mode: 'content' as const,
numFiles: 0,
...(appliedLimit !== undefined && { appliedLimit }),
...(offset > 0 && { appliedOffset: offset }),
}
Promise.all() for Parallel Work
const [skillDirCommands, pluginSkills] = await Promise.all([
getSkillDirCommands(cwd).catch(err => { logError(toError(err)); return [] }),
getPluginSkills().catch(err => { logError(toError(err)); return [] }),
])
Numeric Separators
Use underscores for large numbers:
maxResultSizeChars: 20_000
const MAX_EDIT_FILE_SIZE = 1024 * 1024 * 1024
const ASSISTANT_BLOCKING_BUDGET_MS = 15_000
Destructuring
Function Parameter Destructuring
async call(
{
pattern,
path,
glob,
output_mode = 'files_with_matches',
'-B': context_before,
'-A': context_after,
head_limit,
offset = 0,
},
{ abortController, getAppState },
) { ... }
Inline Destructuring
const { messages, ...paramsWithoutMessages } = params
const { abortController } = context
Renaming in Destructuring
const { '-B': context_before, '-A': context_after } = input
const { numLines: _numLines, ...rest } = output
Type Annotations on Function Returns
Always annotate return types for exported functions:
export function toError(e: unknown): Error { ... }
export function errorMessage(e: unknown): string { ... }
export function getErrnoCode(e: unknown): string | undefined { ... }
export function isAbortError(e: unknown): boolean { ... }
export function isFsInaccessible(e: unknown): e is NodeJS.ErrnoException { ... }
Empty Lines
- One empty line between top-level declarations
- One empty line before
returnin long functions (optional) - No empty lines inside short functions
- One empty line after import blocks
Line Length
No strict line length limit, but:
- Prefer wrapping at ~100-120 characters
- Long strings (descriptions, error messages) can exceed
- Import paths are never wrapped
Object Method Shorthand
// In tool definitions
const tool = {
isConcurrencySafe() { return true },
isReadOnly() { return true },
getPath({ path }): string { return path || getCwd() },
async description() { return getDescription() },
renderToolUseMessage, // Reference to imported function
renderToolResultMessage, // Reference to imported function
}
Arrow Function Formatting
No Braces for Single Expressions
const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
results.map(line => toRelativePath(line))
tools.filter(tool => tool.isEnabled())
Braces for Multi-Statement Bodies
results.map((_, i) => {
const r = stats[i]!
return [_, r.status === 'fulfilled' ? (r.value.mtimeMs ?? 0) : 0] as const
})
switch Statement Formatting
switch (event.type) {
case 'error':
errorLogSink.logError(event.error)
break
case 'mcpError':
errorLogSink.logMCPError(event.serverName, event.error)
break
case 'mcpDebug':
errorLogSink.logMCPDebug(event.serverName, event.message)
break
}
Linting Tools
- Biome — Primary formatter and linter
- ESLint — Additional rules including custom rules
- Custom rules — e.g.,
custom-rules/no-process-exitprevents uncontrolled exits