The Output Style System: Bringing Brand Identity to Terminal Output
A deep dive into Claude Code's output styles — theme system, Markdown terminal rendering, syntax highlighting, and brand consistency
The Problem
Open Claude Code, and you'll notice the terminal has color — not the operating system's defaults, but a carefully designed color scheme. Claude's responses have orange borders, permission requests are purple, diffs use green/red highlighting, and code blocks get syntax highlighting. Switch to /theme light, and all colors seamlessly adapt to a light background. Type /output-style Learning, and Claude's response style shifts from concise to teaching mode, explaining the reasoning behind its decisions.
Behind this are two independent but cooperating systems:
- Visual Theme — Controls colors, borders, syntax highlighting, and other visual presentation, toggled via the
/themecommand - Output Style — Controls the tone and behavioral patterns of Claude AI's responses, selected via the output style picker
This article dives deep into the design and implementation of both systems.
Dual-System Architecture
The key distinction: the visual theme changes what colors you see, while the output style changes what the AI says. The two are completely orthogonal — you can use a dark theme with the Learning style, or a light theme with the default style.
Visual Theme System
Theme Type: 90+ Semantic Colors
This isn't a simple "foreground/background" setup — it's a complete design token system. Each color has clear semantics:
claude— Claude's brand orange, used for AI response borderspermission— Purple, used for permission requestsbashBorder— Pink, used for Bash tool output borderssuccess/error/warning— Semantic status colorsdiffAdded/diffRemoved— Diff highlight colors*Shimmer— Each primary color has a corresponding "shimmer" variant for loading animations
The shimmer variants are a subtle design detail: when the AI is thinking, the border alternates between the primary color and the shimmer color. Without shimmer variants, the animation would be either too jarring (two very different colors) or invisible (the same color).
6 Theme Variants
Each variant is optimized for a specific scenario:
- dark / light — Uses explicit RGB values for consistent appearance across all terminals
- *-daltonized — Color vision deficiency-friendly versions that avoid relying on red/green distinctions
- *-ansi — Uses ANSI color codes instead of RGB, respecting the user's custom terminal color scheme
Why does the light theme use RGB instead of ANSI? Because users might configure ANSI "red" as bright pink or dark maroon in their terminal — if we relied on ANSI colors, diff red and green could become indistinguishable. Using explicit RGB values ensures the colors Anthropic's designers carefully calibrated look the same on any terminal.
The ANSI variants exist because some users invest significant effort in crafting a perfect terminal color scheme and want all tools to use their palette rather than being overridden.
ThemeSetting: The Wisdom of 'auto'
ThemeSetting and ThemeName are different types. ThemeSetting adds an 'auto' option, which resolves to a concrete ThemeName at runtime.
Auto mode queries the terminal's background color via the OSC 11 protocol to determine light or dark mode:
A few implementation details worth noting:
- Feature flag guard — The
AUTO_THEMEfeature flag allows the entiresystemThemeWatchermodule to be dead-code eliminated in external builds - Dynamic import —
import('../../utils/systemThemeWatcher.js')avoids loading unnecessary code when not in auto mode - Cancellation semantics — The
cancelledflag prevents setting state after the component unmounts - $COLORFGBG fallback — On initialization, an approximate value is obtained from the
$COLORFGBGenvironment variable, then corrected by the subsequent OSC 11 query
ThemePicker: An Interactive Component with Live Preview
ThemePicker has a "preview" mechanism — as the user navigates the list, the theme switches in real time so they can see the effect, but it only saves when the user confirms their selection:
The three operations differ as follows:
setPreviewTheme(setting)— Temporarily switches without writing to configsavePreview()— Saves the current preview as the active themecancelPreview()— Reverts to the theme prior to previewing
This lets users see the effect of each option in real time while picking a theme, and pressing Escape returns to the original state.
/theme Command: The Simplest Slash Command
This is a local-jsx type command — it returns a React component rather than plain text. load: () => import('./theme.js') uses dynamic import for on-demand loading.
The actual execution logic is remarkably concise:
The ThemePickerCommand component wraps ThemePicker, calling setTheme(setting) and onDone(Theme set to setting) when the user makes a selection:
Wrapping with Pane color="permission" gives the theme picker a purple border — maintaining visual consistency with other permission/settings interfaces.
Output Style System
Built-in Styles: Default, Explanatory, Learning
The Default style has a value of null — it doesn't inject any additional prompt, and Claude uses its own default behavior. This design avoids the overhead of "even the default mode has a prompt."
keepCodingInstructions: true tells the system to preserve the underlying coding instruction prompt when switching to this style, rather than replacing it entirely. This is important for the Explanatory and Learning styles — they layer teaching capabilities on top of the default behavior, rather than replacing coding capabilities.
Explanatory Style's Insight Format
figures.star comes from the figures library, rendering an appropriate star character across different terminals. The entire Insight block uses backticks to render as monospaced text, ensuring the separator lines align in the terminal.
Learning Style's Interactive Mode
The most interesting part of the Learning style is that it asks the AI to pause and let the user practice:
The prompt also instructs the AI to insert TODO(human) markers in code — a way to create links between the codebase and the conversation. The AI should not continue operating after issuing a "Learn by Doing" request, but instead wait for the user to implement it.
Custom Output Styles: Markdown File Loading
Custom styles are defined through Markdown files placed in the .claude/output-styles/ directory. The loading logic is in loadOutputStylesDir.ts:
Loading Flow in Detail
-
Call
loadMarkdownFilesForSubdir('output-styles', cwd)— This shared utility function simultaneously searches the user directory (~/.claude/output-styles/), project directory (.claude/output-styles/), and policy-managed directories for custom output styles -
Parse frontmatter — The Markdown file's YAML header provides the name and description:
MARKDOWN1---2name: Concise3description: Short and sweet responses4keep-coding-instructions: true5---6Your style prompt content here... -
Filename as fallback — If the frontmatter lacks a
namefield, the filename is used (with the.mdsuffix removed) -
keep-coding-instructionshandling — Supports both boolean and string values (true/'true'), since YAML frontmatter type parsing isn't always consistent -
memoize caching — Uses lodash's
memoizeto avoid redundant filesystem scans
Style Merge Priority
The priority order is built-in < plugin < user < project < managed. This means:
- Projects can define styles with the same name as built-in styles to override them
- Enterprise policies (managed) have the highest priority — even if a project defines a style with the same name, the policy version wins
- Same-name replacement, not merging — later-loaded styles completely replace earlier ones
Markdown Config Loader: Shared Infrastructure
The output style file loading isn't implemented independently; it reuses the shared infrastructure in markdownConfigLoader.ts. This loader simultaneously serves multiple subdirectories including commands, agents, output-styles, and skills:
Directory Traversal Strategy
It traverses upward from the current directory until it hits the git root or the home directory. Stopping at the git root is a security decision — it prevents .claude/ configurations in parent directories from accidentally leaking into child projects.
For example, given this directory structure:
When working in my-repo, if my-repo is a git repository, only concise.md will be loaded — verbose.md from the ~/projects/ level won't be included.
File Search: Dual-Engine Strategy
By default it uses ripgrep to find .md files (faster), but provides a Node.js native implementation as a fallback. The differences between the two search engines:
- ripgrep — Faster, but has higher startup overhead in native builds
- Node.js native — Starts quickly with no external process needed, but slower at scanning large directories
The 3-second timeout (AbortSignal.timeout(3000)) prevents hanging on enormous .claude/output-styles/ directories.
Deduplication: Precise Inode-Level Deduplication
Deduplication uses device:inode identifiers, which can detect paths pointing to the same physical file via symlinks or hard links. For example, if ~/.claude is a symlink to a directory inside the project, the same output-style file might be discovered twice — once as user settings and once as project settings. Inode deduplication ensures it's loaded only once.
getFileIdentity calls lstat with bigint: true because some filesystems (like ExFAT) can have inode numbers that exceed JavaScript's Number precision (53 bits).
Plugin Output Styles
Plugin output styles have a key distinction — namespacing:
Plugin style names are automatically prefixed with pluginName:, e.g., my-plugin:concise. This prevents naming collisions between styles from different plugins.
force-for-plugin Mechanism
Plugins can set force-for-plugin: true in the frontmatter to automatically apply their output style when the plugin is enabled, without requiring the user to manually select it. If multiple plugins set force, only the first is used and a warning is logged:
force-for-plugin only takes effect for plugin-sourced styles. If a user's own output-style file sets this field, a debug-level warning is emitted.
OutputStylePicker: Style Selection UI
It asynchronously loads all styles (including custom and plugin styles), falling back to built-in styles if loading fails. A Loading output styles... message is shown during loading.
mapConfigsToOptions converts style configurations into the format expected by the Select component:
The Default style's config is null, so ?? is needed to provide fallback labels and descriptions.
Cache Clearing: Global Coordination
Multiple layers of memoize caches in the system need coordinated clearing:
getOutputStyleDirStyles— Directory-level style loading cacheloadMarkdownFilesForSubdir— Shared Markdown file search cacheloadPluginOutputStyles— Plugin style cachegetAllOutputStyles— Final merged result cache
clearOutputStyleCaches() clears the first three layers at once, while clearAllOutputStylesCache() clears the top layer. These functions need to be called when the user modifies files under .claude/output-styles/ so that new styles take effect.
.cache?.clear?.() uses optional chaining — if the memoize implementation doesn't expose a cache object, it silently skips without throwing an error.
Analytics Integration: Tracking Style Usage
Analytics events distinguish three cases:
- Default style —
repl_main_thread(no suffix) - Non-default built-in style —
repl_main_thread:outputStyle:Explanatory(includes style name) - Custom style —
repl_main_thread:outputStyle:custom(doesn't leak the user's custom style name)
The privacy consideration in the third case is important — custom style names might contain team names, project names, or other sensitive information.
Style Passing During System Initialization
The system initialization message (system/init) includes the current output style name, passing it to SDK consumers (such as the VS Code extension). This way remote clients know which style the current session uses and can display it in the UI or provide a switching option.
Design Summary
Claude Code's output style system demonstrates several design patterns worth studying:
Separation of concerns — The visual theme and output style are two independent systems controlled through different interfaces. The visual theme is a React Context + CSS-in-JS-style token system; the output style is prompt engineering. Neither depends on the other.
Layered configuration — Built-in < plugin < user < project < policy, where each layer can override the one below. In enterprise environments, policies have the final say.
Security boundaries — The git root prevents parent directory configuration leakage; inode deduplication prevents duplicates caused by symlinks; analytics don't leak custom style names.
Progressive enhancement — The default style has zero overhead (null prompt); custom styles load on demand; when ripgrep is unavailable, the system falls back to Node.js native implementation; if theme selection fails, the current theme is preserved.
Developer experience — Writing a Markdown file and placing it in .claude/output-styles/ creates a custom output style. Frontmatter provides metadata, file content is the prompt. No code changes needed, no config files to modify — the filename is the style name.
This "Markdown as configuration" pattern is widely reused throughout Claude Code — commands, agents, skills, and output styles all use the same markdownConfigLoader infrastructure. A single shared loader serves multiple subsystems, with each subsystem only needing to define its own frontmatter parsing logic and configuration types.