diff --git a/.claude/agents/task-executor.md b/.claude/agents/task-executor.md new file mode 100644 index 000000000..082ccd482 --- /dev/null +++ b/.claude/agents/task-executor.md @@ -0,0 +1,95 @@ +--- +name: task-executor +description: Autonomous task execution agent for lets-intern-client. Implements features, writes tests, commits code, and fixes errors. Delegates from task-runner skill. Use proactively for all coding implementation tasks. +tools: Read, Write, Edit, Bash, Glob, Grep, Task +model: inherit +permissionMode: dontAsk +skills: + - vercel-react-best-practices +hooks: + PostToolUse: + - matcher: 'Edit|Write' + hooks: + - type: command + command: 'INPUT=$(cat); FILE=$(echo "$INPUT" | jq -r ".tool_input.file_path // empty"); [ -z "$FILE" ] || [ ! -f "$FILE" ] && exit 0; case "$FILE" in *.ts|*.tsx|*.js|*.jsx) ;; *) exit 0 ;; esac; npx eslint --fix "$FILE" 2>/dev/null || true; npx prettier --write "$FILE" 2>/dev/null || true' +--- + +# Task Executor — 자율 실행 에이전트 + +task-runner 스킬에서 위임받은 task 파일을 자율적으로 실행합니다. + +## 핵심 원칙 + +1. **사용자에게 묻지 않는다** — 모든 결정은 자율적으로 +2. **Vercel 베스트 프랙티스 항상 적용** — 코드 작성 전 관련 규칙 참조 +3. **오류는 직접 해결** — T3 수정 작업 추가 후 즉시 해결 +4. **커밋 단위로 즉시 커밋** — 하위 작업 완료 시 바로 커밋 + +--- + +## 실행 워크플로우 + +### 코드 작성 전 +``` +1. 관련 파일 탐색 (Glob/Grep) +2. 기존 패턴 파악 (Read) +3. Vercel 규칙 확인: + - 비동기 코드 → async-parallel.md, async-defer-await.md + - 컴포넌트 → rerender-memo.md, rendering-hoist-jsx.md + - 데이터 패칭 → server-parallel-fetching.md, client-swr-dedup.md + - 번들 → bundle-dynamic-imports.md, bundle-barrel-imports.md +``` + +### 코드 작성 후 (자동 처리) +- ESLint + Prettier: PostToolUse 훅이 자동으로 실행 + +### 커밋 단위 완료 시 +```bash +git add [관련 파일들] +git commit -m "type(task N.M): 작업 내용" +``` + +커밋 타입: `feat` | `fix` | `docs` | `style` | `refactor` | `test` | `chore` + +### 테스트 실행 (T2) +```bash +# 가능한 경우 +npx jest [관련-파일] --no-coverage +# 또는 타입 체크 +npx tsc --noEmit +``` + +### 오류 발생 시 +1. T3 항목 추가: `- [ ] N.M.T3 [오류명] 수정` +2. 오류 분석 (Read/Grep) +3. 수정 (Edit) +4. T3 완료 → `[x]` 체크 + +### Push 단위 완료 시 +```bash +git push origin [현재-브랜치] +``` + +--- + +## 작업 파일 체크 규칙 + +- 하위 작업 완료 → 해당 줄 `[ ]` → `[x]` +- 모든 하위 작업 완료 → 상위 작업도 `[x]` +- 완료 보고는 간결하게 (완료 항목 목록 + 커밋 해시) + +--- + +## Vercel 규칙 빠른 참조 + +| 상황 | 적용 규칙 파일 | +|---|---| +| Promise 여러 개 | `async-parallel.md` | +| 컴포넌트 리렌더 최적화 | `rerender-memo.md`, `rerender-derived-state.md` | +| 정적 JSX | `rendering-hoist-jsx.md` | +| 동적 임포트 | `bundle-dynamic-imports.md` | +| barrel import 제거 | `bundle-barrel-imports.md` | +| 서버 데이터 패칭 | `server-parallel-fetching.md` | +| 클라이언트 상태 | `rerender-functional-setstate.md` | + +규칙 파일 위치: `.claude/skills/vercel-react-best-practices/rules/` diff --git a/.claude/agents/test-runner.md b/.claude/agents/test-runner.md new file mode 100644 index 000000000..3e030e9df --- /dev/null +++ b/.claude/agents/test-runner.md @@ -0,0 +1,73 @@ +--- +name: test-runner +description: Test execution and validation agent. Use after completing commit-level implementation tasks. Runs tests, analyzes failures, and reports results. Use proactively after any code changes. +tools: Read, Bash, Grep, Glob, Write, Edit +model: haiku +permissionMode: dontAsk +--- + +# Test Runner — 테스트 실행 에이전트 + +구현 완료된 코드에 대한 테스트를 실행하고 결과를 분석합니다. + +## 실행 순서 + +1. **테스트 범위 파악** + - 수정된 파일 확인 (`git diff --name-only HEAD~1`) + - 관련 테스트 파일 탐색 (`*.test.ts`, `*.spec.ts`, `*.test.tsx`) + +2. **타입 체크 먼저** + ```bash + npx tsc --noEmit 2>&1 | head -30 + ``` + +3. **단위 테스트 실행** + ```bash + # 특정 파일 테스트 + npx jest [파일명] --no-coverage --passWithNoTests 2>&1 | tail -20 + + # 전체 테스트 + npx jest --no-coverage --passWithNoTests 2>&1 | tail -30 + ``` + +4. **테스트 파일 없는 경우** + - 관련 컴포넌트/함수에 대한 기본 테스트 코드 작성 + - 파일 위치: `[대상파일].test.ts(x)` 또는 `__tests__/[파일명].test.ts(x)` + +## 테스트 코드 작성 기준 + +```typescript +// 컴포넌트 테스트 (React Testing Library) +import { render, screen } from '@testing-library/react'; +import { ComponentName } from './ComponentName'; + +describe('ComponentName', () => { + it('renders correctly', () => { + render(); + // assertions + }); +}); + +// 유틸 함수 테스트 +describe('utilFunction', () => { + it('handles expected input', () => { + expect(utilFunction(input)).toBe(expected); + }); +}); +``` + +## 결과 보고 형식 + +``` +테스트 결과: +- 타입 오류: N개 (있으면 파일:줄 명시) +- 테스트 통과: N개 / 전체 M개 +- 실패: [파일명:테스트명] - [오류 요약] +- 권장 조치: [있는 경우만] +``` + +## 오류 발생 시 + +1. 오류 메시지 분석 +2. 수정 가능한 경우: 직접 수정 후 재실행 +3. 설계 문제인 경우: 오류 내용 + 수정 제안을 task 파일에 T3로 추가 diff --git a/.claude/docs/claude_code_docs/Automate workflows with hooks.md b/.claude/docs/claude_code_docs/Automate workflows with hooks.md new file mode 100644 index 000000000..2c34bfbf6 --- /dev/null +++ b/.claude/docs/claude_code_docs/Automate workflows with hooks.md @@ -0,0 +1,671 @@ +> ## Documentation Index +> +> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt +> Use this file to discover all available pages before exploring further. + +# Automate workflows with hooks + +> Run shell commands automatically when Claude Code edits files, finishes tasks, or needs input. Format code, send notifications, validate commands, and enforce project rules. + +Hooks are user-defined shell commands that execute at specific points in Claude Code's lifecycle. They provide deterministic control over Claude Code's behavior, ensuring certain actions always happen rather than relying on the LLM to choose to run them. Use hooks to enforce project rules, automate repetitive tasks, and integrate Claude Code with your existing tools. + +For decisions that require judgment rather than deterministic rules, you can also use [prompt-based hooks](#prompt-based-hooks) or [agent-based hooks](#agent-based-hooks) that use a Claude model to evaluate conditions. + +For other ways to extend Claude Code, see [skills](/en/skills) for giving Claude additional instructions and executable commands, [subagents](/en/sub-agents) for running tasks in isolated contexts, and [plugins](/en/plugins) for packaging extensions to share across projects. + + + This guide covers common use cases and how to get started. For full event schemas, JSON input/output formats, and advanced features like async hooks and MCP tool hooks, see the [Hooks reference](/en/hooks). + + +## Set up your first hook + +The fastest way to create a hook is through the `/hooks` interactive menu in Claude Code. This walkthrough creates a desktop notification hook, so you get alerted whenever Claude is waiting for your input instead of watching the terminal. + + + + Type `/hooks` in the Claude Code CLI. You'll see a list of all available hook events, plus an option to disable all hooks. Each event corresponds to a point in Claude's lifecycle where you can run custom code. Select `Notification` to create a hook that fires when Claude needs your attention. + + + + The menu shows a list of matchers, which filter when the hook fires. Set the matcher to `*` to fire on all notification types. You can narrow it later by changing the matcher to a specific value like `permission_prompt` or `idle_prompt`. + + + + Select `+ Add new hook…`. The menu prompts you for a shell command to run when the event fires. Hooks run any shell command you provide, so you can use your platform's built-in notification tool. Copy the command for your OS: + + + + Uses [`osascript`](https://ss64.com/mac/osascript.html) to trigger a native macOS notification through AppleScript: + + ``` + osascript -e 'display notification "Claude Code needs your attention" with title "Claude Code"' + ``` + + + + Uses `notify-send`, which is pre-installed on most Linux desktops with a notification daemon: + + ``` + notify-send 'Claude Code' 'Claude Code needs your attention' + ``` + + + + Uses PowerShell to show a native message box through .NET's Windows Forms: + + ``` + powershell.exe -Command "[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude Code needs your attention', 'Claude Code')" + ``` + + + + + + + The menu asks where to save the hook configuration. Select `User settings` to store it in `~/.claude/settings.json`, which applies the hook to all your projects. You could also choose `Project settings` to scope it to the current project. See [Configure hook location](#configure-hook-location) for all available scopes. + + + + Press `Esc` to return to the CLI. Ask Claude to do something that requires permission, then switch away from the terminal. You should receive a desktop notification. + + + +## What you can automate + +Hooks let you run code at key points in Claude Code's lifecycle: format files after edits, block commands before they execute, send notifications when Claude needs input, inject context at session start, and more. For the full list of hook events, see the [Hooks reference](/en/hooks#hook-lifecycle). + +Each example includes a ready-to-use configuration block that you add to a [settings file](#configure-hook-location). The most common patterns: + +- [Get notified when Claude needs input](#get-notified-when-claude-needs-input) +- [Auto-format code after edits](#auto-format-code-after-edits) +- [Block edits to protected files](#block-edits-to-protected-files) +- [Re-inject context after compaction](#re-inject-context-after-compaction) +- [Audit configuration changes](#audit-configuration-changes) + +### Get notified when Claude needs input + +Get a desktop notification whenever Claude finishes working and needs your input, so you can switch to other tasks without checking the terminal. + +This hook uses the `Notification` event, which fires when Claude is waiting for input or permission. Each tab below uses the platform's native notification command. Add this to `~/.claude/settings.json`, or use the [interactive walkthrough](#set-up-your-first-hook) above to configure it with `/hooks`: + + + + ```json theme={null} + { + "hooks": { + "Notification": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'" + } + ] + } + ] + } + } + ``` + + + + ```json theme={null} + { + "hooks": { + "Notification": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "notify-send 'Claude Code' 'Claude Code needs your attention'" + } + ] + } + ] + } + } + ``` + + + + ```json theme={null} + { + "hooks": { + "Notification": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "powershell.exe -Command \"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude Code needs your attention', 'Claude Code')\"" + } + ] + } + ] + } + } + ``` + + + +### Auto-format code after edits + +Automatically run [Prettier](https://prettier.io/) on every file Claude edits, so formatting stays consistent without manual intervention. + +This hook uses the `PostToolUse` event with an `Edit|Write` matcher, so it runs only after file-editing tools. The command extracts the edited file path with [`jq`](https://jqlang.github.io/jq/) and passes it to Prettier. Add this to `.claude/settings.json` in your project root: + +```json theme={null} +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" + } + ] + } + ] + } +} +``` + + + The Bash examples on this page use `jq` for JSON parsing. Install it with `brew install jq` (macOS), `apt-get install jq` (Debian/Ubuntu), or see [`jq` downloads](https://jqlang.github.io/jq/download/). + + +### Block edits to protected files + +Prevent Claude from modifying sensitive files like `.env`, `package-lock.json`, or anything in `.git/`. Claude receives feedback explaining why the edit was blocked, so it can adjust its approach. + +This example uses a separate script file that the hook calls. The script checks the target file path against a list of protected patterns and exits with code 2 to block the edit. + + + + Save this to `.claude/hooks/protect-files.sh`: + + ```bash theme={null} + #!/bin/bash + # protect-files.sh + + INPUT=$(cat) + FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') + + PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/") + + for pattern in "${PROTECTED_PATTERNS[@]}"; do + if [[ "$FILE_PATH" == *"$pattern"* ]]; then + echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2 + exit 2 + fi + done + + exit 0 + ``` + + + + + Hook scripts must be executable for Claude Code to run them: + + ```bash theme={null} + chmod +x .claude/hooks/protect-files.sh + ``` + + + + + Add a `PreToolUse` hook to `.claude/settings.json` that runs the script before any `Edit` or `Write` tool call: + + ```json theme={null} + { + "hooks": { + "PreToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh" + } + ] + } + ] + } + } + ``` + + + + +### Re-inject context after compaction + +When Claude's context window fills up, compaction summarizes the conversation to free space. This can lose important details. Use a `SessionStart` hook with a `compact` matcher to re-inject critical context after every compaction. + +Any text your command writes to stdout is added to Claude's context. This example reminds Claude of project conventions and recent work. Add this to `.claude/settings.json` in your project root: + +```json theme={null} +{ + "hooks": { + "SessionStart": [ + { + "matcher": "compact", + "hooks": [ + { + "type": "command", + "command": "echo 'Reminder: use Bun, not npm. Run bun test before committing. Current sprint: auth refactor.'" + } + ] + } + ] + } +} +``` + +You can replace the `echo` with any command that produces dynamic output, like `git log --oneline -5` to show recent commits. For injecting context on every session start, consider using [CLAUDE.md](/en/memory) instead. For environment variables, see [`CLAUDE_ENV_FILE`](/en/hooks#persist-environment-variables) in the reference. + +### Audit configuration changes + +Track when settings or skills files change during a session. The `ConfigChange` event fires when an external process or editor modifies a configuration file, so you can log changes for compliance or block unauthorized modifications. + +This example appends each change to an audit log. Add this to `~/.claude/settings.json`: + +```json theme={null} +{ + "hooks": { + "ConfigChange": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "jq -c '{timestamp: now | todate, source: .source, file: .file_path}' >> ~/claude-config-audit.log" + } + ] + } + ] + } +} +``` + +The matcher filters by configuration type: `user_settings`, `project_settings`, `local_settings`, `policy_settings`, or `skills`. To block a change from taking effect, exit with code 2 or return `{"decision": "block"}`. See the [ConfigChange reference](/en/hooks#configchange) for the full input schema. + +## How hooks work + +Hook events fire at specific lifecycle points in Claude Code. When an event fires, all matching hooks run in parallel, and identical hook commands are automatically deduplicated. The table below shows each event and when it triggers: + +| Event | When it fires | +| :------------------- | :---------------------------------------------------------------------------------------------------------- | +| `SessionStart` | When a session begins or resumes | +| `UserPromptSubmit` | When you submit a prompt, before Claude processes it | +| `PreToolUse` | Before a tool call executes. Can block it | +| `PermissionRequest` | When a permission dialog appears | +| `PostToolUse` | After a tool call succeeds | +| `PostToolUseFailure` | After a tool call fails | +| `Notification` | When Claude Code sends a notification | +| `SubagentStart` | When a subagent is spawned | +| `SubagentStop` | When a subagent finishes | +| `Stop` | When Claude finishes responding | +| `TeammateIdle` | When an [agent team](/en/agent-teams) teammate is about to go idle | +| `TaskCompleted` | When a task is being marked as completed | +| `ConfigChange` | When a configuration file changes during a session | +| `WorktreeCreate` | When a worktree is being created via `--worktree` or `isolation: "worktree"`. Replaces default git behavior | +| `WorktreeRemove` | When a worktree is being removed, either at session exit or when a subagent finishes | +| `PreCompact` | Before context compaction | +| `SessionEnd` | When a session terminates | + +Each hook has a `type` that determines how it runs. Most hooks use `"type": "command"`, which runs a shell command. Two other options use a Claude model to make decisions: `"type": "prompt"` for single-turn evaluation and `"type": "agent"` for multi-turn verification with tool access. See [Prompt-based hooks](#prompt-based-hooks) and [Agent-based hooks](#agent-based-hooks) for details. + +### Read input and return output + +Hooks communicate with Claude Code through stdin, stdout, stderr, and exit codes. When an event fires, Claude Code passes event-specific data as JSON to your script's stdin. Your script reads that data, does its work, and tells Claude Code what to do next via the exit code. + +#### Hook input + +Every event includes common fields like `session_id` and `cwd`, but each event type adds different data. For example, when Claude runs a Bash command, a `PreToolUse` hook receives something like this on stdin: + +```json theme={null} +{ + "session_id": "abc123", // unique ID for this session + "cwd": "/Users/sarah/myproject", // working directory when the event fired + "hook_event_name": "PreToolUse", // which event triggered this hook + "tool_name": "Bash", // the tool Claude is about to use + "tool_input": { + // the arguments Claude passed to the tool + "command": "npm test" // for Bash, this is the shell command + } +} +``` + +Your script can parse that JSON and act on any of those fields. `UserPromptSubmit` hooks get the `prompt` text instead, `SessionStart` hooks get the `source` (startup, resume, clear, compact), and so on. See [Common input fields](/en/hooks#common-input-fields) in the reference for shared fields, and each event's section for event-specific schemas. + +#### Hook output + +Your script tells Claude Code what to do next by writing to stdout or stderr and exiting with a specific code. For example, a `PreToolUse` hook that wants to block a command: + +```bash theme={null} +#!/bin/bash +INPUT=$(cat) +COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command') + +if echo "$COMMAND" | grep -q "drop table"; then + echo "Blocked: dropping tables is not allowed" >&2 # stderr becomes Claude's feedback + exit 2 # exit 2 = block the action +fi + +exit 0 # exit 0 = let it proceed +``` + +The exit code determines what happens next: + +- **Exit 0**: the action proceeds. For `UserPromptSubmit` and `SessionStart` hooks, anything you write to stdout is added to Claude's context. +- **Exit 2**: the action is blocked. Write a reason to stderr, and Claude receives it as feedback so it can adjust. +- **Any other exit code**: the action proceeds. Stderr is logged but not shown to Claude. Toggle verbose mode with `Ctrl+O` to see these messages in the transcript. + +#### Structured JSON output + +Exit codes give you two options: allow or block. For more control, exit 0 and print a JSON object to stdout instead. + + + Use exit 2 to block with a stderr message, or exit 0 with JSON for structured control. Don't mix them: Claude Code ignores JSON when you exit 2. + + +For example, a `PreToolUse` hook can deny a tool call and tell Claude why, or escalate it to the user for approval: + +```json theme={null} +{ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "deny", + "permissionDecisionReason": "Use rg instead of grep for better performance" + } +} +``` + +Claude Code reads `permissionDecision` and cancels the tool call, then feeds `permissionDecisionReason` back to Claude as feedback. These three options are specific to `PreToolUse`: + +- `"allow"`: proceed without showing a permission prompt +- `"deny"`: cancel the tool call and send the reason to Claude +- `"ask"`: show the permission prompt to the user as normal + +Other events use different decision patterns. For example, `PostToolUse` and `Stop` hooks use a top-level `decision: "block"` field, while `PermissionRequest` uses `hookSpecificOutput.decision.behavior`. See the [summary table](/en/hooks#decision-control) in the reference for a full breakdown by event. + +For `UserPromptSubmit` hooks, use `additionalContext` instead to inject text into Claude's context. Prompt-based hooks (`type: "prompt"`) handle output differently: see [Prompt-based hooks](#prompt-based-hooks). + +### Filter hooks with matchers + +Without a matcher, a hook fires on every occurrence of its event. Matchers let you narrow that down. For example, if you want to run a formatter only after file edits (not after every tool call), add a matcher to your `PostToolUse` hook: + +```json theme={null} +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [{ "type": "command", "command": "prettier --write ..." }] + } + ] + } +} +``` + +The `"Edit|Write"` matcher is a regex pattern that matches the tool name. The hook only fires when Claude uses the `Edit` or `Write` tool, not when it uses `Bash`, `Read`, or any other tool. + +Each event type matches on a specific field. Matchers support exact strings and regex patterns: + +| Event | What the matcher filters | Example matcher values | +| :---------------------------------------------------------------------------------------------- | :------------------------ | :--------------------------------------------------------------------------------- | +| `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionRequest` | tool name | `Bash`, `Edit\|Write`, `mcp__.*` | +| `SessionStart` | how the session started | `startup`, `resume`, `clear`, `compact` | +| `SessionEnd` | why the session ended | `clear`, `logout`, `prompt_input_exit`, `bypass_permissions_disabled`, `other` | +| `Notification` | notification type | `permission_prompt`, `idle_prompt`, `auth_success`, `elicitation_dialog` | +| `SubagentStart` | agent type | `Bash`, `Explore`, `Plan`, or custom agent names | +| `PreCompact` | what triggered compaction | `manual`, `auto` | +| `SubagentStop` | agent type | same values as `SubagentStart` | +| `ConfigChange` | configuration source | `user_settings`, `project_settings`, `local_settings`, `policy_settings`, `skills` | +| `UserPromptSubmit`, `Stop`, `TeammateIdle`, `TaskCompleted`, `WorktreeCreate`, `WorktreeRemove` | no matcher support | always fires on every occurrence | + +A few more examples showing matchers on different event types: + + + + Match only `Bash` tool calls and log each command to a file. The `PostToolUse` event fires after the command completes, so `tool_input.command` contains what ran. The hook receives the event data as JSON on stdin, and `jq -r '.tool_input.command'` extracts just the command string, which `>>` appends to the log file: + + ```json theme={null} + { + "hooks": { + "PostToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "jq -r '.tool_input.command' >> ~/.claude/command-log.txt" + } + ] + } + ] + } + } + ``` + + + + + MCP tools use a different naming convention than built-in tools: `mcp____`, where `` is the MCP server name and `` is the tool it provides. For example, `mcp__github__search_repositories` or `mcp__filesystem__read_file`. Use a regex matcher to target all tools from a specific server, or match across servers with a pattern like `mcp__.*__write.*`. See [Match MCP tools](/en/hooks#match-mcp-tools) in the reference for the full list of examples. + + The command below extracts the tool name from the hook's JSON input with `jq` and writes it to stderr, where it shows up in verbose mode (`Ctrl+O`): + + ```json theme={null} + { + "hooks": { + "PreToolUse": [ + { + "matcher": "mcp__github__.*", + "hooks": [ + { + "type": "command", + "command": "echo \"GitHub tool called: $(jq -r '.tool_name')\" >&2" + } + ] + } + ] + } + } + ``` + + + + + The `SessionEnd` event supports matchers on the reason the session ended. This hook only fires on `clear` (when you run `/clear`), not on normal exits: + + ```json theme={null} + { + "hooks": { + "SessionEnd": [ + { + "matcher": "clear", + "hooks": [ + { + "type": "command", + "command": "rm -f /tmp/claude-scratch-*.txt" + } + ] + } + ] + } + } + ``` + + + + +For full matcher syntax, see the [Hooks reference](/en/hooks#configuration). + +### Configure hook location + +Where you add a hook determines its scope: + +| Location | Scope | Shareable | +| :--------------------------------------------------------- | :--------------------------------- | :--------------------------------- | +| `~/.claude/settings.json` | All your projects | No, local to your machine | +| `.claude/settings.json` | Single project | Yes, can be committed to the repo | +| `.claude/settings.local.json` | Single project | No, gitignored | +| Managed policy settings | Organization-wide | Yes, admin-controlled | +| [Plugin](/en/plugins) `hooks/hooks.json` | When plugin is enabled | Yes, bundled with the plugin | +| [Skill](/en/skills) or [agent](/en/sub-agents) frontmatter | While the skill or agent is active | Yes, defined in the component file | + +You can also use the [`/hooks` menu](/en/hooks#the-hooks-menu) in Claude Code to add, delete, and view hooks interactively. To disable all hooks at once, use the toggle at the bottom of the `/hooks` menu or set `"disableAllHooks": true` in your settings file. + +Hooks added through the `/hooks` menu take effect immediately. If you edit settings files directly while Claude Code is running, the changes won't take effect until you review them in the `/hooks` menu or restart your session. + +## Prompt-based hooks + +For decisions that require judgment rather than deterministic rules, use `type: "prompt"` hooks. Instead of running a shell command, Claude Code sends your prompt and the hook's input data to a Claude model (Haiku by default) to make the decision. You can specify a different model with the `model` field if you need more capability. + +The model's only job is to return a yes/no decision as JSON: + +- `"ok": true`: the action proceeds +- `"ok": false`: the action is blocked. The model's `"reason"` is fed back to Claude so it can adjust. + +This example uses a `Stop` hook to ask the model whether all requested tasks are complete. If the model returns `"ok": false`, Claude keeps working and uses the `reason` as its next instruction: + +```json theme={null} +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "prompt", + "prompt": "Check if all tasks are complete. If not, respond with {\"ok\": false, \"reason\": \"what remains to be done\"}." + } + ] + } + ] + } +} +``` + +For full configuration options, see [Prompt-based hooks](/en/hooks#prompt-based-hooks) in the reference. + +## Agent-based hooks + +When verification requires inspecting files or running commands, use `type: "agent"` hooks. Unlike prompt hooks which make a single LLM call, agent hooks spawn a subagent that can read files, search code, and use other tools to verify conditions before returning a decision. + +Agent hooks use the same `"ok"` / `"reason"` response format as prompt hooks, but with a longer default timeout of 60 seconds and up to 50 tool-use turns. + +This example verifies that tests pass before allowing Claude to stop: + +```json theme={null} +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "agent", + "prompt": "Verify that all unit tests pass. Run the test suite and check the results. $ARGUMENTS", + "timeout": 120 + } + ] + } + ] + } +} +``` + +Use prompt hooks when the hook input data alone is enough to make a decision. Use agent hooks when you need to verify something against the actual state of the codebase. + +For full configuration options, see [Agent-based hooks](/en/hooks#agent-based-hooks) in the reference. + +## Limitations and troubleshooting + +### Limitations + +- Hooks communicate through stdout, stderr, and exit codes only. They cannot trigger slash commands or tool calls directly. +- Hook timeout is 10 minutes by default, configurable per hook with the `timeout` field (in seconds). +- `PostToolUse` hooks cannot undo actions since the tool has already executed. +- `PermissionRequest` hooks do not fire in [non-interactive mode](/en/headless) (`-p`). Use `PreToolUse` hooks for automated permission decisions. +- `Stop` hooks fire whenever Claude finishes responding, not only at task completion. They do not fire on user interrupts. + +### Hook not firing + +The hook is configured but never executes. + +- Run `/hooks` and confirm the hook appears under the correct event +- Check that the matcher pattern matches the tool name exactly (matchers are case-sensitive) +- Verify you're triggering the right event type (e.g., `PreToolUse` fires before tool execution, `PostToolUse` fires after) +- If using `PermissionRequest` hooks in non-interactive mode (`-p`), switch to `PreToolUse` instead + +### Hook error in output + +You see a message like "PreToolUse hook error: ..." in the transcript. + +- Your script exited with a non-zero code unexpectedly. Test it manually by piping sample JSON: + ```bash theme={null} + echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh + echo $? # Check the exit code + ``` +- If you see "command not found", use absolute paths or `$CLAUDE_PROJECT_DIR` to reference scripts +- If you see "jq: command not found", install `jq` or use Python/Node.js for JSON parsing +- If the script isn't running at all, make it executable: `chmod +x ./my-hook.sh` + +### `/hooks` shows no hooks configured + +You edited a settings file but the hooks don't appear in the menu. + +- Restart your session or open `/hooks` to reload. Hooks added through the `/hooks` menu take effect immediately, but manual file edits require a reload. +- Verify your JSON is valid (trailing commas and comments are not allowed) +- Confirm the settings file is in the correct location: `.claude/settings.json` for project hooks, `~/.claude/settings.json` for global hooks + +### Stop hook runs forever + +Claude keeps working in an infinite loop instead of stopping. + +Your Stop hook script needs to check whether it already triggered a continuation. Parse the `stop_hook_active` field from the JSON input and exit early if it's `true`: + +```bash theme={null} +#!/bin/bash +INPUT=$(cat) +if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then + exit 0 # Allow Claude to stop +fi +# ... rest of your hook logic +``` + +### JSON validation failed + +Claude Code shows a JSON parsing error even though your hook script outputs valid JSON. + +When Claude Code runs a hook, it spawns a shell that sources your profile (`~/.zshrc` or `~/.bashrc`). If your profile contains unconditional `echo` statements, that output gets prepended to your hook's JSON: + +``` +Shell ready on arm64 +{"decision": "block", "reason": "Not allowed"} +``` + +Claude Code tries to parse this as JSON and fails. To fix this, wrap echo statements in your shell profile so they only run in interactive shells: + +```bash theme={null} +# In ~/.zshrc or ~/.bashrc +if [[ $- == *i* ]]; then + echo "Shell ready" +fi +``` + +The `$-` variable contains shell flags, and `i` means interactive. Hooks run in non-interactive shells, so the echo is skipped. + +### Debug techniques + +Toggle verbose mode with `Ctrl+O` to see hook output in the transcript, or run `claude --debug` for full execution details including which hooks matched and their exit codes. + +## Learn more + +- [Hooks reference](/en/hooks): full event schemas, JSON output format, async hooks, and MCP tool hooks +- [Security considerations](/en/hooks#security-considerations): review before deploying hooks in shared or production environments +- [Bash command validator example](https://github.com/anthropics/claude-code/blob/main/examples/hooks/bash_command_validator_example.py): complete reference implementation diff --git a/.claude/docs/claude_code_docs/Create custom subagents.md b/.claude/docs/claude_code_docs/Create custom subagents.md new file mode 100644 index 000000000..ea9842188 --- /dev/null +++ b/.claude/docs/claude_code_docs/Create custom subagents.md @@ -0,0 +1,830 @@ +> ## Documentation Index +> +> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt +> Use this file to discover all available pages before exploring further. + +# Create custom subagents + +> Create and use specialized AI subagents in Claude Code for task-specific workflows and improved context management. + +Subagents are specialized AI assistants that handle specific types of tasks. Each subagent runs in its own context window with a custom system prompt, specific tool access, and independent permissions. When Claude encounters a task that matches a subagent's description, it delegates to that subagent, which works independently and returns results. + + + If you need multiple agents working in parallel and communicating with each other, see [agent teams](/en/agent-teams) instead. Subagents work within a single session; agent teams coordinate across separate sessions. + + +Subagents help you: + +- **Preserve context** by keeping exploration and implementation out of your main conversation +- **Enforce constraints** by limiting which tools a subagent can use +- **Reuse configurations** across projects with user-level subagents +- **Specialize behavior** with focused system prompts for specific domains +- **Control costs** by routing tasks to faster, cheaper models like Haiku + +Claude uses each subagent's description to decide when to delegate tasks. When you create a subagent, write a clear description so Claude knows when to use it. + +Claude Code includes several built-in subagents like **Explore**, **Plan**, and **general-purpose**. You can also create custom subagents to handle specific tasks. This page covers the [built-in subagents](#built-in-subagents), [how to create your own](#quickstart-create-your-first-subagent), [full configuration options](#configure-subagents), [patterns for working with subagents](#work-with-subagents), and [example subagents](#example-subagents). + +## Built-in subagents + +Claude Code includes built-in subagents that Claude automatically uses when appropriate. Each inherits the parent conversation's permissions with additional tool restrictions. + + + + A fast, read-only agent optimized for searching and analyzing codebases. + + * **Model**: Haiku (fast, low-latency) + * **Tools**: Read-only tools (denied access to Write and Edit tools) + * **Purpose**: File discovery, code search, codebase exploration + + Claude delegates to Explore when it needs to search or understand a codebase without making changes. This keeps exploration results out of your main conversation context. + + When invoking Explore, Claude specifies a thoroughness level: **quick** for targeted lookups, **medium** for balanced exploration, or **very thorough** for comprehensive analysis. + + + + + A research agent used during [plan mode](/en/common-workflows#use-plan-mode-for-safe-code-analysis) to gather context before presenting a plan. + + * **Model**: Inherits from main conversation + * **Tools**: Read-only tools (denied access to Write and Edit tools) + * **Purpose**: Codebase research for planning + + When you're in plan mode and Claude needs to understand your codebase, it delegates research to the Plan subagent. This prevents infinite nesting (subagents cannot spawn other subagents) while still gathering necessary context. + + + + + A capable agent for complex, multi-step tasks that require both exploration and action. + + * **Model**: Inherits from main conversation + * **Tools**: All tools + * **Purpose**: Complex research, multi-step operations, code modifications + + Claude delegates to general-purpose when the task requires both exploration and modification, complex reasoning to interpret results, or multiple dependent steps. + + + + + Claude Code includes additional helper agents for specific tasks. These are typically invoked automatically, so you don't need to use them directly. + + | Agent | Model | When Claude uses it | + | :---------------- | :------- | :------------------------------------------------------- | + | Bash | Inherits | Running terminal commands in a separate context | + | statusline-setup | Sonnet | When you run `/statusline` to configure your status line | + | Claude Code Guide | Haiku | When you ask questions about Claude Code features | + + + + +Beyond these built-in subagents, you can create your own with custom prompts, tool restrictions, permission modes, hooks, and skills. The following sections show how to get started and customize subagents. + +## Quickstart: create your first subagent + +Subagents are defined in Markdown files with YAML frontmatter. You can [create them manually](#write-subagent-files) or use the `/agents` command. + +This walkthrough guides you through creating a user-level subagent with the `/agent` command. The subagent reviews code and suggests improvements for the codebase. + + + + In Claude Code, run: + + ``` + /agents + ``` + + + + + Select **Create new agent**, then choose **User-level**. This saves the subagent to `~/.claude/agents/` so it's available in all your projects. + + + + Select **Generate with Claude**. When prompted, describe the subagent: + + ``` + A code improvement agent that scans files and suggests improvements + for readability, performance, and best practices. It should explain + each issue, show the current code, and provide an improved version. + ``` + + Claude generates the system prompt and configuration. Press `e` to open it in your editor if you want to customize it. + + + + + For a read-only reviewer, deselect everything except **Read-only tools**. If you keep all tools selected, the subagent inherits all tools available to the main conversation. + + + + Choose which model the subagent uses. For this example agent, select **Sonnet**, which balances capability and speed for analyzing code patterns. + + + + Pick a background color for the subagent. This helps you identify which subagent is running in the UI. + + + + Save the subagent. It's available immediately (no restart needed). Try it: + + ``` + Use the code-improver agent to suggest improvements in this project + ``` + + Claude delegates to your new subagent, which scans the codebase and returns improvement suggestions. + + + + +You now have a subagent you can use in any project on your machine to analyze codebases and suggest improvements. + +You can also create subagents manually as Markdown files, define them via CLI flags, or distribute them through plugins. The following sections cover all configuration options. + +## Configure subagents + +### Use the /agents command + +The `/agents` command provides an interactive interface for managing subagents. Run `/agents` to: + +- View all available subagents (built-in, user, project, and plugin) +- Create new subagents with guided setup or Claude generation +- Edit existing subagent configuration and tool access +- Delete custom subagents +- See which subagents are active when duplicates exist + +This is the recommended way to create and manage subagents. For manual creation or automation, you can also add subagent files directly. + +To list all configured subagents from the command line without starting an interactive session, run `claude agents`. This shows agents grouped by source and indicates which are overridden by higher-priority definitions. + +### Choose the subagent scope + +Subagents are Markdown files with YAML frontmatter. Store them in different locations depending on scope. When multiple subagents share the same name, the higher-priority location wins. + +| Location | Scope | Priority | How to create | +| :--------------------------- | :---------------------- | :---------- | :------------------------------------ | +| `--agents` CLI flag | Current session | 1 (highest) | Pass JSON when launching Claude Code | +| `.claude/agents/` | Current project | 2 | Interactive or manual | +| `~/.claude/agents/` | All your projects | 3 | Interactive or manual | +| Plugin's `agents/` directory | Where plugin is enabled | 4 (lowest) | Installed with [plugins](/en/plugins) | + +**Project subagents** (`.claude/agents/`) are ideal for subagents specific to a codebase. Check them into version control so your team can use and improve them collaboratively. + +**User subagents** (`~/.claude/agents/`) are personal subagents available in all your projects. + +**CLI-defined subagents** are passed as JSON when launching Claude Code. They exist only for that session and aren't saved to disk, making them useful for quick testing or automation scripts: + +```bash theme={null} +claude --agents '{ + "code-reviewer": { + "description": "Expert code reviewer. Use proactively after code changes.", + "prompt": "You are a senior code reviewer. Focus on code quality, security, and best practices.", + "tools": ["Read", "Grep", "Glob", "Bash"], + "model": "sonnet" + } +}' +``` + +The `--agents` flag accepts JSON with the same [frontmatter](#supported-frontmatter-fields) fields as file-based subagents: `description`, `prompt`, `tools`, `disallowedTools`, `model`, `permissionMode`, `mcpServers`, `hooks`, `maxTurns`, `skills`, and `memory`. Use `prompt` for the system prompt, equivalent to the markdown body in file-based subagents. See the [CLI reference](/en/cli-reference#agents-flag-format) for the full JSON format. + +**Plugin subagents** come from [plugins](/en/plugins) you've installed. They appear in `/agents` alongside your custom subagents. See the [plugin components reference](/en/plugins-reference#agents) for details on creating plugin subagents. + +### Write subagent files + +Subagent files use YAML frontmatter for configuration, followed by the system prompt in Markdown: + + + Subagents are loaded at session start. If you create a subagent by manually adding a file, restart your session or use `/agents` to load it immediately. + + +```markdown theme={null} +--- +name: code-reviewer +description: Reviews code for quality and best practices +tools: Read, Glob, Grep +model: sonnet +--- + +You are a code reviewer. When invoked, analyze the code and provide +specific, actionable feedback on quality, security, and best practices. +``` + +The frontmatter defines the subagent's metadata and configuration. The body becomes the system prompt that guides the subagent's behavior. Subagents receive only this system prompt (plus basic environment details like working directory), not the full Claude Code system prompt. + +#### Supported frontmatter fields + +The following fields can be used in the YAML frontmatter. Only `name` and `description` are required. + +| Field | Required | Description | +| :---------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | Yes | Unique identifier using lowercase letters and hyphens | +| `description` | Yes | When Claude should delegate to this subagent | +| `tools` | No | [Tools](#available-tools) the subagent can use. Inherits all tools if omitted | +| `disallowedTools` | No | Tools to deny, removed from inherited or specified list | +| `model` | No | [Model](#choose-a-model) to use: `sonnet`, `opus`, `haiku`, or `inherit`. Defaults to `inherit` | +| `permissionMode` | No | [Permission mode](#permission-modes): `default`, `acceptEdits`, `dontAsk`, `bypassPermissions`, or `plan` | +| `maxTurns` | No | Maximum number of agentic turns before the subagent stops | +| `skills` | No | [Skills](/en/skills) to load into the subagent's context at startup. The full skill content is injected, not just made available for invocation. Subagents don't inherit skills from the parent conversation | +| `mcpServers` | No | [MCP servers](/en/mcp) available to this subagent. Each entry is either a server name referencing an already-configured server (e.g., `"slack"`) or an inline definition with the server name as key and a full [MCP server config](/en/mcp#configure-mcp-servers) as value | +| `hooks` | No | [Lifecycle hooks](#define-hooks-for-subagents) scoped to this subagent | +| `memory` | No | [Persistent memory scope](#enable-persistent-memory): `user`, `project`, or `local`. Enables cross-session learning | +| `background` | No | Set to `true` to always run this subagent as a [background task](#run-subagents-in-foreground-or-background). Default: `false` | +| `isolation` | No | Set to `worktree` to run the subagent in a temporary [git worktree](/en/common-workflows#run-parallel-claude-code-sessions-with-git-worktrees), giving it an isolated copy of the repository. The worktree is automatically cleaned up if the subagent makes no changes | + +### Choose a model + +The `model` field controls which [AI model](/en/model-config) the subagent uses: + +- **Model alias**: Use one of the available aliases: `sonnet`, `opus`, or `haiku` +- **inherit**: Use the same model as the main conversation +- **Omitted**: If not specified, defaults to `inherit` (uses the same model as the main conversation) + +### Control subagent capabilities + +You can control what subagents can do through tool access, permission modes, and conditional rules. + +#### Available tools + +Subagents can use any of Claude Code's [internal tools](/en/settings#tools-available-to-claude). By default, subagents inherit all tools from the main conversation, including MCP tools. + +To restrict tools, use the `tools` field (allowlist) or `disallowedTools` field (denylist): + +```yaml theme={null} +--- +name: safe-researcher +description: Research agent with restricted capabilities +tools: Read, Grep, Glob, Bash +disallowedTools: Write, Edit +--- +``` + +#### Restrict which subagents can be spawned + +When an agent runs as the main thread with `claude --agent`, it can spawn subagents using the Task tool. To restrict which subagent types it can spawn, use `Task(agent_type)` syntax in the `tools` field: + +```yaml theme={null} +--- +name: coordinator +description: Coordinates work across specialized agents +tools: Task(worker, researcher), Read, Bash +--- +``` + +This is an allowlist: only the `worker` and `researcher` subagents can be spawned. If the agent tries to spawn any other type, the request fails and the agent sees only the allowed types in its prompt. To block specific agents while allowing all others, use [`permissions.deny`](#disable-specific-subagents) instead. + +To allow spawning any subagent without restrictions, use `Task` without parentheses: + +```yaml theme={null} +tools: Task, Read, Bash +``` + +If `Task` is omitted from the `tools` list entirely, the agent cannot spawn any subagents. This restriction only applies to agents running as the main thread with `claude --agent`. Subagents cannot spawn other subagents, so `Task(agent_type)` has no effect in subagent definitions. + +#### Permission modes + +The `permissionMode` field controls how the subagent handles permission prompts. Subagents inherit the permission context from the main conversation but can override the mode. + +| Mode | Behavior | +| :------------------ | :----------------------------------------------------------------- | +| `default` | Standard permission checking with prompts | +| `acceptEdits` | Auto-accept file edits | +| `dontAsk` | Auto-deny permission prompts (explicitly allowed tools still work) | +| `bypassPermissions` | Skip all permission checks | +| `plan` | Plan mode (read-only exploration) | + + + Use `bypassPermissions` with caution. It skips all permission checks, allowing the subagent to execute any operation without approval. + + +If the parent uses `bypassPermissions`, this takes precedence and cannot be overridden. + +#### Preload skills into subagents + +Use the `skills` field to inject skill content into a subagent's context at startup. This gives the subagent domain knowledge without requiring it to discover and load skills during execution. + +```yaml theme={null} +--- +name: api-developer +description: Implement API endpoints following team conventions +skills: + - api-conventions + - error-handling-patterns +--- +Implement API endpoints. Follow the conventions and patterns from the preloaded skills. +``` + +The full content of each skill is injected into the subagent's context, not just made available for invocation. Subagents don't inherit skills from the parent conversation; you must list them explicitly. + + + This is the inverse of [running a skill in a subagent](/en/skills#run-skills-in-a-subagent). With `skills` in a subagent, the subagent controls the system prompt and loads skill content. With `context: fork` in a skill, the skill content is injected into the agent you specify. Both use the same underlying system. + + +#### Enable persistent memory + +The `memory` field gives the subagent a persistent directory that survives across conversations. The subagent uses this directory to build up knowledge over time, such as codebase patterns, debugging insights, and architectural decisions. + +```yaml theme={null} +--- +name: code-reviewer +description: Reviews code for quality and best practices +memory: user +--- +You are a code reviewer. As you review code, update your agent memory with +patterns, conventions, and recurring issues you discover. +``` + +Choose a scope based on how broadly the memory should apply: + +| Scope | Location | Use when | +| :-------- | :-------------------------------------------- | :------------------------------------------------------------------------------------------ | +| `user` | `~/.claude/agent-memory//` | the subagent should remember learnings across all projects | +| `project` | `.claude/agent-memory//` | the subagent's knowledge is project-specific and shareable via version control | +| `local` | `.claude/agent-memory-local//` | the subagent's knowledge is project-specific but should not be checked into version control | + +When memory is enabled: + +- The subagent's system prompt includes instructions for reading and writing to the memory directory. +- The subagent's system prompt also includes the first 200 lines of `MEMORY.md` in the memory directory, with instructions to curate `MEMORY.md` if it exceeds 200 lines. +- Read, Write, and Edit tools are automatically enabled so the subagent can manage its memory files. + +##### Persistent memory tips + +- `user` is the recommended default scope. Use `project` or `local` when the subagent's knowledge is only relevant to a specific codebase. +- Ask the subagent to consult its memory before starting work: "Review this PR, and check your memory for patterns you've seen before." +- Ask the subagent to update its memory after completing a task: "Now that you're done, save what you learned to your memory." Over time, this builds a knowledge base that makes the subagent more effective. +- Include memory instructions directly in the subagent's markdown file so it proactively maintains its own knowledge base: + + ```markdown theme={null} + Update your agent memory as you discover codepaths, patterns, library + locations, and key architectural decisions. This builds up institutional + knowledge across conversations. Write concise notes about what you found + and where. + ``` + +#### Conditional rules with hooks + +For more dynamic control over tool usage, use `PreToolUse` hooks to validate operations before they execute. This is useful when you need to allow some operations of a tool while blocking others. + +This example creates a subagent that only allows read-only database queries. The `PreToolUse` hook runs the script specified in `command` before each Bash command executes: + +```yaml theme={null} +--- +name: db-reader +description: Execute read-only database queries +tools: Bash +hooks: + PreToolUse: + - matcher: 'Bash' + hooks: + - type: command + command: './scripts/validate-readonly-query.sh' +--- +``` + +Claude Code [passes hook input as JSON](/en/hooks#pretooluse-input) via stdin to hook commands. The validation script reads this JSON, extracts the Bash command, and [exits with code 2](/en/hooks#exit-code-2-behavior-per-event) to block write operations: + +```bash theme={null} +#!/bin/bash +# ./scripts/validate-readonly-query.sh + +INPUT=$(cat) +COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') + +# Block SQL write operations (case-insensitive) +if echo "$COMMAND" | grep -iE '\b(INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|TRUNCATE)\b' > /dev/null; then + echo "Blocked: Only SELECT queries are allowed" >&2 + exit 2 +fi + +exit 0 +``` + +See [Hook input](/en/hooks#pretooluse-input) for the complete input schema and [exit codes](/en/hooks#exit-code-output) for how exit codes affect behavior. + +#### Disable specific subagents + +You can prevent Claude from using specific subagents by adding them to the `deny` array in your [settings](/en/settings#permission-settings). Use the format `Task(subagent-name)` where `subagent-name` matches the subagent's name field. + +```json theme={null} +{ + "permissions": { + "deny": ["Task(Explore)", "Task(my-custom-agent)"] + } +} +``` + +This works for both built-in and custom subagents. You can also use the `--disallowedTools` CLI flag: + +```bash theme={null} +claude --disallowedTools "Task(Explore)" +``` + +See [Permissions documentation](/en/permissions#tool-specific-permission-rules) for more details on permission rules. + +### Define hooks for subagents + +Subagents can define [hooks](/en/hooks) that run during the subagent's lifecycle. There are two ways to configure hooks: + +1. **In the subagent's frontmatter**: Define hooks that run only while that subagent is active +2. **In `settings.json`**: Define hooks that run in the main session when subagents start or stop + +#### Hooks in subagent frontmatter + +Define hooks directly in the subagent's markdown file. These hooks only run while that specific subagent is active and are cleaned up when it finishes. + +All [hook events](/en/hooks#hook-events) are supported. The most common events for subagents are: + +| Event | Matcher input | When it fires | +| :------------ | :------------ | :------------------------------------------------------------------ | +| `PreToolUse` | Tool name | Before the subagent uses a tool | +| `PostToolUse` | Tool name | After the subagent uses a tool | +| `Stop` | (none) | When the subagent finishes (converted to `SubagentStop` at runtime) | + +This example validates Bash commands with the `PreToolUse` hook and runs a linter after file edits with `PostToolUse`: + +```yaml theme={null} +--- +name: code-reviewer +description: Review code changes with automatic linting +hooks: + PreToolUse: + - matcher: 'Bash' + hooks: + - type: command + command: './scripts/validate-command.sh $TOOL_INPUT' + PostToolUse: + - matcher: 'Edit|Write' + hooks: + - type: command + command: './scripts/run-linter.sh' +--- +``` + +`Stop` hooks in frontmatter are automatically converted to `SubagentStop` events. + +#### Project-level hooks for subagent events + +Configure hooks in `settings.json` that respond to subagent lifecycle events in the main session. + +| Event | Matcher input | When it fires | +| :-------------- | :-------------- | :------------------------------- | +| `SubagentStart` | Agent type name | When a subagent begins execution | +| `SubagentStop` | Agent type name | When a subagent completes | + +Both events support matchers to target specific agent types by name. This example runs a setup script only when the `db-agent` subagent starts, and a cleanup script when any subagent stops: + +```json theme={null} +{ + "hooks": { + "SubagentStart": [ + { + "matcher": "db-agent", + "hooks": [ + { "type": "command", "command": "./scripts/setup-db-connection.sh" } + ] + } + ], + "SubagentStop": [ + { + "hooks": [ + { "type": "command", "command": "./scripts/cleanup-db-connection.sh" } + ] + } + ] + } +} +``` + +See [Hooks](/en/hooks) for the complete hook configuration format. + +## Work with subagents + +### Understand automatic delegation + +Claude automatically delegates tasks based on the task description in your request, the `description` field in subagent configurations, and current context. To encourage proactive delegation, include phrases like "use proactively" in your subagent's description field. + +You can also request a specific subagent explicitly: + +``` +Use the test-runner subagent to fix failing tests +Have the code-reviewer subagent look at my recent changes +``` + +### Run subagents in foreground or background + +Subagents can run in the foreground (blocking) or background (concurrent): + +- **Foreground subagents** block the main conversation until complete. Permission prompts and clarifying questions (like [`AskUserQuestion`](/en/settings#tools-available-to-claude)) are passed through to you. +- **Background subagents** run concurrently while you continue working. Before launching, Claude Code prompts for any tool permissions the subagent will need, ensuring it has the necessary approvals upfront. Once running, the subagent inherits these permissions and auto-denies anything not pre-approved. If a background subagent needs to ask clarifying questions, that tool call fails but the subagent continues. MCP tools are not available in background subagents. + +If a background subagent fails due to missing permissions, you can [resume it](#resume-subagents) in the foreground to retry with interactive prompts. + +Claude decides whether to run subagents in the foreground or background based on the task. You can also: + +- Ask Claude to "run this in the background" +- Press **Ctrl+B** to background a running task + +To disable all background task functionality, set the `CLAUDE_CODE_DISABLE_BACKGROUND_TASKS` environment variable to `1`. See [Environment variables](/en/settings#environment-variables). + +### Common patterns + +#### Isolate high-volume operations + +One of the most effective uses for subagents is isolating operations that produce large amounts of output. Running tests, fetching documentation, or processing log files can consume significant context. By delegating these to a subagent, the verbose output stays in the subagent's context while only the relevant summary returns to your main conversation. + +``` +Use a subagent to run the test suite and report only the failing tests with their error messages +``` + +#### Run parallel research + +For independent investigations, spawn multiple subagents to work simultaneously: + +``` +Research the authentication, database, and API modules in parallel using separate subagents +``` + +Each subagent explores its area independently, then Claude synthesizes the findings. This works best when the research paths don't depend on each other. + + + When subagents complete, their results return to your main conversation. Running many subagents that each return detailed results can consume significant context. + + +For tasks that need sustained parallelism or exceed your context window, [agent teams](/en/agent-teams) give each worker its own independent context. + +#### Chain subagents + +For multi-step workflows, ask Claude to use subagents in sequence. Each subagent completes its task and returns results to Claude, which then passes relevant context to the next subagent. + +``` +Use the code-reviewer subagent to find performance issues, then use the optimizer subagent to fix them +``` + +### Choose between subagents and main conversation + +Use the **main conversation** when: + +- The task needs frequent back-and-forth or iterative refinement +- Multiple phases share significant context (planning → implementation → testing) +- You're making a quick, targeted change +- Latency matters. Subagents start fresh and may need time to gather context + +Use **subagents** when: + +- The task produces verbose output you don't need in your main context +- You want to enforce specific tool restrictions or permissions +- The work is self-contained and can return a summary + +Consider [Skills](/en/skills) instead when you want reusable prompts or workflows that run in the main conversation context rather than isolated subagent context. + + + Subagents cannot spawn other subagents. If your workflow requires nested delegation, use [Skills](/en/skills) or [chain subagents](#chain-subagents) from the main conversation. + + +### Manage subagent context + +#### Resume subagents + +Each subagent invocation creates a new instance with fresh context. To continue an existing subagent's work instead of starting over, ask Claude to resume it. + +Resumed subagents retain their full conversation history, including all previous tool calls, results, and reasoning. The subagent picks up exactly where it stopped rather than starting fresh. + +When a subagent completes, Claude receives its agent ID. To resume a subagent, ask Claude to continue the previous work: + +``` +Use the code-reviewer subagent to review the authentication module +[Agent completes] + +Continue that code review and now analyze the authorization logic +[Claude resumes the subagent with full context from previous conversation] +``` + +You can also ask Claude for the agent ID if you want to reference it explicitly, or find IDs in the transcript files at `~/.claude/projects/{project}/{sessionId}/subagents/`. Each transcript is stored as `agent-{agentId}.jsonl`. + +Subagent transcripts persist independently of the main conversation: + +- **Main conversation compaction**: When the main conversation compacts, subagent transcripts are unaffected. They're stored in separate files. +- **Session persistence**: Subagent transcripts persist within their session. You can [resume a subagent](#resume-subagents) after restarting Claude Code by resuming the same session. +- **Automatic cleanup**: Transcripts are cleaned up based on the `cleanupPeriodDays` setting (default: 30 days). + +#### Auto-compaction + +Subagents support automatic compaction using the same logic as the main conversation. By default, auto-compaction triggers at approximately 95% capacity. To trigger compaction earlier, set `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` to a lower percentage (for example, `50`). See [environment variables](/en/settings#environment-variables) for details. + +Compaction events are logged in subagent transcript files: + +```json theme={null} +{ + "type": "system", + "subtype": "compact_boundary", + "compactMetadata": { + "trigger": "auto", + "preTokens": 167189 + } +} +``` + +The `preTokens` value shows how many tokens were used before compaction occurred. + +## Example subagents + +These examples demonstrate effective patterns for building subagents. Use them as starting points, or generate a customized version with Claude. + + + **Best practices:** + +- **Design focused subagents:** each subagent should excel at one specific task +- **Write detailed descriptions:** Claude uses the description to decide when to delegate +- **Limit tool access:** grant only necessary permissions for security and focus +- **Check into version control:** share project subagents with your team + + +### Code reviewer + +A read-only subagent that reviews code without modifying it. This example shows how to design a focused subagent with limited tool access (no Edit or Write) and a detailed prompt that specifies exactly what to look for and how to format output. + +```markdown theme={null} +--- +name: code-reviewer +description: Expert code review specialist. Proactively reviews code for quality, security, and maintainability. Use immediately after writing or modifying code. +tools: Read, Grep, Glob, Bash +model: inherit +--- + +You are a senior code reviewer ensuring high standards of code quality and security. + +When invoked: + +1. Run git diff to see recent changes +2. Focus on modified files +3. Begin review immediately + +Review checklist: + +- Code is clear and readable +- Functions and variables are well-named +- No duplicated code +- Proper error handling +- No exposed secrets or API keys +- Input validation implemented +- Good test coverage +- Performance considerations addressed + +Provide feedback organized by priority: + +- Critical issues (must fix) +- Warnings (should fix) +- Suggestions (consider improving) + +Include specific examples of how to fix issues. +``` + +### Debugger + +A subagent that can both analyze and fix issues. Unlike the code reviewer, this one includes Edit because fixing bugs requires modifying code. The prompt provides a clear workflow from diagnosis to verification. + +```markdown theme={null} +--- +name: debugger +description: Debugging specialist for errors, test failures, and unexpected behavior. Use proactively when encountering any issues. +tools: Read, Edit, Bash, Grep, Glob +--- + +You are an expert debugger specializing in root cause analysis. + +When invoked: + +1. Capture error message and stack trace +2. Identify reproduction steps +3. Isolate the failure location +4. Implement minimal fix +5. Verify solution works + +Debugging process: + +- Analyze error messages and logs +- Check recent code changes +- Form and test hypotheses +- Add strategic debug logging +- Inspect variable states + +For each issue, provide: + +- Root cause explanation +- Evidence supporting the diagnosis +- Specific code fix +- Testing approach +- Prevention recommendations + +Focus on fixing the underlying issue, not the symptoms. +``` + +### Data scientist + +A domain-specific subagent for data analysis work. This example shows how to create subagents for specialized workflows outside of typical coding tasks. It explicitly sets `model: sonnet` for more capable analysis. + +```markdown theme={null} +--- +name: data-scientist +description: Data analysis expert for SQL queries, BigQuery operations, and data insights. Use proactively for data analysis tasks and queries. +tools: Bash, Read, Write +model: sonnet +--- + +You are a data scientist specializing in SQL and BigQuery analysis. + +When invoked: + +1. Understand the data analysis requirement +2. Write efficient SQL queries +3. Use BigQuery command line tools (bq) when appropriate +4. Analyze and summarize results +5. Present findings clearly + +Key practices: + +- Write optimized SQL queries with proper filters +- Use appropriate aggregations and joins +- Include comments explaining complex logic +- Format results for readability +- Provide data-driven recommendations + +For each analysis: + +- Explain the query approach +- Document any assumptions +- Highlight key findings +- Suggest next steps based on data + +Always ensure queries are efficient and cost-effective. +``` + +### Database query validator + +A subagent that allows Bash access but validates commands to permit only read-only SQL queries. This example shows how to use `PreToolUse` hooks for conditional validation when you need finer control than the `tools` field provides. + +```markdown theme={null} +--- +name: db-reader +description: Execute read-only database queries. Use when analyzing data or generating reports. +tools: Bash +hooks: + PreToolUse: + - matcher: 'Bash' + hooks: + - type: command + command: './scripts/validate-readonly-query.sh' +--- + +You are a database analyst with read-only access. Execute SELECT queries to answer questions about the data. + +When asked to analyze data: + +1. Identify which tables contain the relevant data +2. Write efficient SELECT queries with appropriate filters +3. Present results clearly with context + +You cannot modify data. If asked to INSERT, UPDATE, DELETE, or modify schema, explain that you only have read access. +``` + +Claude Code [passes hook input as JSON](/en/hooks#pretooluse-input) via stdin to hook commands. The validation script reads this JSON, extracts the command being executed, and checks it against a list of SQL write operations. If a write operation is detected, the script [exits with code 2](/en/hooks#exit-code-2-behavior-per-event) to block execution and returns an error message to Claude via stderr. + +Create the validation script anywhere in your project. The path must match the `command` field in your hook configuration: + +```bash theme={null} +#!/bin/bash +# Blocks SQL write operations, allows SELECT queries + +# Read JSON input from stdin +INPUT=$(cat) + +# Extract the command field from tool_input using jq +COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') + +if [ -z "$COMMAND" ]; then + exit 0 +fi + +# Block write operations (case-insensitive) +if echo "$COMMAND" | grep -iE '\b(INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|TRUNCATE|REPLACE|MERGE)\b' > /dev/null; then + echo "Blocked: Write operations not allowed. Use SELECT queries only." >&2 + exit 2 +fi + +exit 0 +``` + +Make the script executable: + +```bash theme={null} +chmod +x ./scripts/validate-readonly-query.sh +``` + +The hook receives JSON via stdin with the Bash command in `tool_input.command`. Exit code 2 blocks the operation and feeds the error message back to Claude. See [Hooks](/en/hooks#exit-code-output) for details on exit codes and [Hook input](/en/hooks#pretooluse-input) for the complete input schema. + +## Next steps + +Now that you understand subagents, explore these related features: + +- [Distribute subagents with plugins](/en/plugins) to share subagents across teams or projects +- [Run Claude Code programmatically](/en/headless) with the Agent SDK for CI/CD and automation +- [Use MCP servers](/en/mcp) to give subagents access to external tools and data diff --git a/.claude/docs/claude_code_docs/Extend Claude with skills.md b/.claude/docs/claude_code_docs/Extend Claude with skills.md new file mode 100644 index 000000000..370ef9567 --- /dev/null +++ b/.claude/docs/claude_code_docs/Extend Claude with skills.md @@ -0,0 +1,677 @@ +> ## Documentation Index +> +> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt +> Use this file to discover all available pages before exploring further. + +# Extend Claude with skills + +> Create, manage, and share skills to extend Claude's capabilities in Claude Code. Includes custom slash commands. + +Skills extend what Claude can do. Create a `SKILL.md` file with instructions, and Claude adds it to its toolkit. Claude uses skills when relevant, or you can invoke one directly with `/skill-name`. + + + For built-in commands like `/help` and `/compact`, see [interactive mode](/en/interactive-mode#built-in-commands). + +**Custom slash commands have been merged into skills.** A file at `.claude/commands/review.md` and a skill at `.claude/skills/review/SKILL.md` both create `/review` and work the same way. Your existing `.claude/commands/` files keep working. Skills add optional features: a directory for supporting files, frontmatter to [control whether you or Claude invokes them](#control-who-invokes-a-skill), and the ability for Claude to load them automatically when relevant. + + +Claude Code skills follow the [Agent Skills](https://agentskills.io) open standard, which works across multiple AI tools. Claude Code extends the standard with additional features like [invocation control](#control-who-invokes-a-skill), [subagent execution](#run-skills-in-a-subagent), and [dynamic context injection](#inject-dynamic-context). + +## Getting started + +### Create your first skill + +This example creates a skill that teaches Claude to explain code using visual diagrams and analogies. Since it uses default frontmatter, Claude can load it automatically when you ask how something works, or you can invoke it directly with `/explain-code`. + + + + Create a directory for the skill in your personal skills folder. Personal skills are available across all your projects. + + ```bash theme={null} + mkdir -p ~/.claude/skills/explain-code + ``` + + + + + Every skill needs a `SKILL.md` file with two parts: YAML frontmatter (between `---` markers) that tells Claude when to use the skill, and markdown content with instructions Claude follows when the skill is invoked. The `name` field becomes the `/slash-command`, and the `description` helps Claude decide when to load it automatically. + + Create `~/.claude/skills/explain-code/SKILL.md`: + + ```yaml theme={null} + --- + name: explain-code + description: Explains code with visual diagrams and analogies. Use when explaining how code works, teaching about a codebase, or when the user asks "how does this work?" + --- + + When explaining code, always include: + + 1. **Start with an analogy**: Compare the code to something from everyday life + 2. **Draw a diagram**: Use ASCII art to show the flow, structure, or relationships + 3. **Walk through the code**: Explain step-by-step what happens + 4. **Highlight a gotcha**: What's a common mistake or misconception? + + Keep explanations conversational. For complex concepts, use multiple analogies. + ``` + + + + + You can test it two ways: + + **Let Claude invoke it automatically** by asking something that matches the description: + + ``` + How does this code work? + ``` + + **Or invoke it directly** with the skill name: + + ``` + /explain-code src/auth/login.ts + ``` + + Either way, Claude should include an analogy and ASCII diagram in its explanation. + + + + +### Where skills live + +Where you store a skill determines who can use it: + +| Location | Path | Applies to | +| :--------- | :------------------------------------------------------- | :----------------------------- | +| Enterprise | See [managed settings](/en/permissions#managed-settings) | All users in your organization | +| Personal | `~/.claude/skills//SKILL.md` | All your projects | +| Project | `.claude/skills//SKILL.md` | This project only | +| Plugin | `/skills//SKILL.md` | Where plugin is enabled | + +When skills share the same name across levels, higher-priority locations win: enterprise > personal > project. Plugin skills use a `plugin-name:skill-name` namespace, so they cannot conflict with other levels. If you have files in `.claude/commands/`, those work the same way, but if a skill and a command share the same name, the skill takes precedence. + +#### Automatic discovery from nested directories + +When you work with files in subdirectories, Claude Code automatically discovers skills from nested `.claude/skills/` directories. For example, if you're editing a file in `packages/frontend/`, Claude Code also looks for skills in `packages/frontend/.claude/skills/`. This supports monorepo setups where packages have their own skills. + +Each skill is a directory with `SKILL.md` as the entrypoint: + +``` +my-skill/ +├── SKILL.md # Main instructions (required) +├── template.md # Template for Claude to fill in +├── examples/ +│ └── sample.md # Example output showing expected format +└── scripts/ + └── validate.sh # Script Claude can execute +``` + +The `SKILL.md` contains the main instructions and is required. Other files are optional and let you build more powerful skills: templates for Claude to fill in, example outputs showing the expected format, scripts Claude can execute, or detailed reference documentation. Reference these files from your `SKILL.md` so Claude knows what they contain and when to load them. See [Add supporting files](#add-supporting-files) for more details. + + + Files in `.claude/commands/` still work and support the same [frontmatter](#frontmatter-reference). Skills are recommended since they support additional features like supporting files. + + +#### Skills from additional directories + +Skills defined in `.claude/skills/` within directories added via `--add-dir` are loaded automatically and picked up by live change detection, so you can edit them during a session without restarting. + + + CLAUDE.md files from `--add-dir` directories are not loaded by default. To load them, set `CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1`. See [Load memory from additional directories](/en/memory#load-memory-from-additional-directories). + + +## Configure skills + +Skills are configured through YAML frontmatter at the top of `SKILL.md` and the markdown content that follows. + +### Types of skill content + +Skill files can contain any instructions, but thinking about how you want to invoke them helps guide what to include: + +**Reference content** adds knowledge Claude applies to your current work. Conventions, patterns, style guides, domain knowledge. This content runs inline so Claude can use it alongside your conversation context. + +```yaml theme={null} +--- +name: api-conventions +description: API design patterns for this codebase +--- +When writing API endpoints: + - Use RESTful naming conventions + - Return consistent error formats + - Include request validation +``` + +**Task content** gives Claude step-by-step instructions for a specific action, like deployments, commits, or code generation. These are often actions you want to invoke directly with `/skill-name` rather than letting Claude decide when to run them. Add `disable-model-invocation: true` to prevent Claude from triggering it automatically. + +```yaml theme={null} +--- +name: deploy +description: Deploy the application to production +context: fork +disable-model-invocation: true +--- + +Deploy the application: +1. Run the test suite +2. Build the application +3. Push to the deployment target +``` + +Your `SKILL.md` can contain anything, but thinking through how you want the skill invoked (by you, by Claude, or both) and where you want it to run (inline or in a subagent) helps guide what to include. For complex skills, you can also [add supporting files](#add-supporting-files) to keep the main skill focused. + +### Frontmatter reference + +Beyond the markdown content, you can configure skill behavior using YAML frontmatter fields between `---` markers at the top of your `SKILL.md` file: + +```yaml theme={null} +--- +name: my-skill +description: What this skill does +disable-model-invocation: true +allowed-tools: Read, Grep +--- +Your skill instructions here... +``` + +All fields are optional. Only `description` is recommended so Claude knows when to use the skill. + +| Field | Required | Description | +| :------------------------- | :---------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | No | Display name for the skill. If omitted, uses the directory name. Lowercase letters, numbers, and hyphens only (max 64 characters). | +| `description` | Recommended | What the skill does and when to use it. Claude uses this to decide when to apply the skill. If omitted, uses the first paragraph of markdown content. | +| `argument-hint` | No | Hint shown during autocomplete to indicate expected arguments. Example: `[issue-number]` or `[filename] [format]`. | +| `disable-model-invocation` | No | Set to `true` to prevent Claude from automatically loading this skill. Use for workflows you want to trigger manually with `/name`. Default: `false`. | +| `user-invocable` | No | Set to `false` to hide from the `/` menu. Use for background knowledge users shouldn't invoke directly. Default: `true`. | +| `allowed-tools` | No | Tools Claude can use without asking permission when this skill is active. | +| `model` | No | Model to use when this skill is active. | +| `context` | No | Set to `fork` to run in a forked subagent context. | +| `agent` | No | Which subagent type to use when `context: fork` is set. | +| `hooks` | No | Hooks scoped to this skill's lifecycle. See [Hooks in skills and agents](/en/hooks#hooks-in-skills-and-agents) for configuration format. | + +#### Available string substitutions + +Skills support string substitution for dynamic values in the skill content: + +| Variable | Description | +| :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------- | +| `$ARGUMENTS` | All arguments passed when invoking the skill. If `$ARGUMENTS` is not present in the content, arguments are appended as `ARGUMENTS: `. | +| `$ARGUMENTS[N]` | Access a specific argument by 0-based index, such as `$ARGUMENTS[0]` for the first argument. | +| `$N` | Shorthand for `$ARGUMENTS[N]`, such as `$0` for the first argument or `$1` for the second. | +| `${CLAUDE_SESSION_ID}` | The current session ID. Useful for logging, creating session-specific files, or correlating skill output with sessions. | + +**Example using substitutions:** + +```yaml theme={null} +--- +name: session-logger +description: Log activity for this session +--- + +Log the following to logs/${CLAUDE_SESSION_ID}.log: + +$ARGUMENTS +``` + +### Add supporting files + +Skills can include multiple files in their directory. This keeps `SKILL.md` focused on the essentials while letting Claude access detailed reference material only when needed. Large reference docs, API specifications, or example collections don't need to load into context every time the skill runs. + +``` +my-skill/ +├── SKILL.md (required - overview and navigation) +├── reference.md (detailed API docs - loaded when needed) +├── examples.md (usage examples - loaded when needed) +└── scripts/ + └── helper.py (utility script - executed, not loaded) +``` + +Reference supporting files from `SKILL.md` so Claude knows what each file contains and when to load it: + +```markdown theme={null} +## Additional resources + +- For complete API details, see [reference.md](reference.md) +- For usage examples, see [examples.md](examples.md) +``` + +Keep `SKILL.md` under 500 lines. Move detailed reference material to separate files. + +### Control who invokes a skill + +By default, both you and Claude can invoke any skill. You can type `/skill-name` to invoke it directly, and Claude can load it automatically when relevant to your conversation. Two frontmatter fields let you restrict this: + +- **`disable-model-invocation: true`**: Only you can invoke the skill. Use this for workflows with side effects or that you want to control timing, like `/commit`, `/deploy`, or `/send-slack-message`. You don't want Claude deciding to deploy because your code looks ready. + +- **`user-invocable: false`**: Only Claude can invoke the skill. Use this for background knowledge that isn't actionable as a command. A `legacy-system-context` skill explains how an old system works. Claude should know this when relevant, but `/legacy-system-context` isn't a meaningful action for users to take. + +This example creates a deploy skill that only you can trigger. The `disable-model-invocation: true` field prevents Claude from running it automatically: + +```yaml theme={null} +--- +name: deploy +description: Deploy the application to production +disable-model-invocation: true +--- + +Deploy $ARGUMENTS to production: + +1. Run the test suite +2. Build the application +3. Push to the deployment target +4. Verify the deployment succeeded +``` + +Here's how the two fields affect invocation and context loading: + +| Frontmatter | You can invoke | Claude can invoke | When loaded into context | +| :------------------------------- | :------------- | :---------------- | :----------------------------------------------------------- | +| (default) | Yes | Yes | Description always in context, full skill loads when invoked | +| `disable-model-invocation: true` | Yes | No | Description not in context, full skill loads when you invoke | +| `user-invocable: false` | No | Yes | Description always in context, full skill loads when invoked | + + + In a regular session, skill descriptions are loaded into context so Claude knows what's available, but full skill content only loads when invoked. [Subagents with preloaded skills](/en/sub-agents#preload-skills-into-subagents) work differently: the full skill content is injected at startup. + + +### Restrict tool access + +Use the `allowed-tools` field to limit which tools Claude can use when a skill is active. This skill creates a read-only mode where Claude can explore files but not modify them: + +```yaml theme={null} +--- +name: safe-reader +description: Read files without making changes +allowed-tools: Read, Grep, Glob +--- +``` + +### Pass arguments to skills + +Both you and Claude can pass arguments when invoking a skill. Arguments are available via the `$ARGUMENTS` placeholder. + +This skill fixes a GitHub issue by number. The `$ARGUMENTS` placeholder gets replaced with whatever follows the skill name: + +```yaml theme={null} +--- +name: fix-issue +description: Fix a GitHub issue +disable-model-invocation: true +--- +Fix GitHub issue $ARGUMENTS following our coding standards. + +1. Read the issue description +2. Understand the requirements +3. Implement the fix +4. Write tests +5. Create a commit +``` + +When you run `/fix-issue 123`, Claude receives "Fix GitHub issue 123 following our coding standards..." + +If you invoke a skill with arguments but the skill doesn't include `$ARGUMENTS`, Claude Code appends `ARGUMENTS: ` to the end of the skill content so Claude still sees what you typed. + +To access individual arguments by position, use `$ARGUMENTS[N]` or the shorter `$N`: + +```yaml theme={null} +--- +name: migrate-component +description: Migrate a component from one framework to another +--- +Migrate the $ARGUMENTS[0] component from $ARGUMENTS[1] to $ARGUMENTS[2]. +Preserve all existing behavior and tests. +``` + +Running `/migrate-component SearchBar React Vue` replaces `$ARGUMENTS[0]` with `SearchBar`, `$ARGUMENTS[1]` with `React`, and `$ARGUMENTS[2]` with `Vue`. The same skill using the `$N` shorthand: + +```yaml theme={null} +--- +name: migrate-component +description: Migrate a component from one framework to another +--- +Migrate the $0 component from $1 to $2. +Preserve all existing behavior and tests. +``` + +## Advanced patterns + +### Inject dynamic context + +The `!`command\`\` syntax runs shell commands before the skill content is sent to Claude. The command output replaces the placeholder, so Claude receives actual data, not the command itself. + +This skill summarizes a pull request by fetching live PR data with the GitHub CLI. The `!`gh pr diff\`\` and other commands run first, and their output gets inserted into the prompt: + +```yaml theme={null} +--- +name: pr-summary +description: Summarize changes in a pull request +context: fork +agent: Explore +allowed-tools: Bash(gh *) +--- + +## Pull request context +- PR diff: !`gh pr diff` +- PR comments: !`gh pr view --comments` +- Changed files: !`gh pr diff --name-only` + +## Your task +Summarize this pull request... +``` + +When this skill runs: + +1. Each `!`command\`\` executes immediately (before Claude sees anything) +2. The output replaces the placeholder in the skill content +3. Claude receives the fully-rendered prompt with actual PR data + +This is preprocessing, not something Claude executes. Claude only sees the final result. + + + To enable [extended thinking](/en/common-workflows#use-extended-thinking-thinking-mode) in a skill, include the word "ultrathink" anywhere in your skill content. + + +### Run skills in a subagent + +Add `context: fork` to your frontmatter when you want a skill to run in isolation. The skill content becomes the prompt that drives the subagent. It won't have access to your conversation history. + + + `context: fork` only makes sense for skills with explicit instructions. If your skill contains guidelines like "use these API conventions" without a task, the subagent receives the guidelines but no actionable prompt, and returns without meaningful output. + + +Skills and [subagents](/en/sub-agents) work together in two directions: + +| Approach | System prompt | Task | Also loads | +| :--------------------------- | :---------------------------------------- | :-------------------------- | :--------------------------- | +| Skill with `context: fork` | From agent type (`Explore`, `Plan`, etc.) | SKILL.md content | CLAUDE.md | +| Subagent with `skills` field | Subagent's markdown body | Claude's delegation message | Preloaded skills + CLAUDE.md | + +With `context: fork`, you write the task in your skill and pick an agent type to execute it. For the inverse (defining a custom subagent that uses skills as reference material), see [Subagents](/en/sub-agents#preload-skills-into-subagents). + +#### Example: Research skill using Explore agent + +This skill runs research in a forked Explore agent. The skill content becomes the task, and the agent provides read-only tools optimized for codebase exploration: + +```yaml theme={null} +--- +name: deep-research +description: Research a topic thoroughly +context: fork +agent: Explore +--- + +Research $ARGUMENTS thoroughly: + +1. Find relevant files using Glob and Grep +2. Read and analyze the code +3. Summarize findings with specific file references +``` + +When this skill runs: + +1. A new isolated context is created +2. The subagent receives the skill content as its prompt ("Research \$ARGUMENTS thoroughly...") +3. The `agent` field determines the execution environment (model, tools, and permissions) +4. Results are summarized and returned to your main conversation + +The `agent` field specifies which subagent configuration to use. Options include built-in agents (`Explore`, `Plan`, `general-purpose`) or any custom subagent from `.claude/agents/`. If omitted, uses `general-purpose`. + +### Restrict Claude's skill access + +By default, Claude can invoke any skill that doesn't have `disable-model-invocation: true` set. Skills that define `allowed-tools` grant Claude access to those tools without per-use approval when the skill is active. Your [permission settings](/en/permissions) still govern baseline approval behavior for all other tools. Built-in commands like `/compact` and `/init` are not available through the Skill tool. + +Three ways to control which skills Claude can invoke: + +**Disable all skills** by denying the Skill tool in `/permissions`: + +``` +# Add to deny rules: +Skill +``` + +**Allow or deny specific skills** using [permission rules](/en/permissions): + +``` +# Allow only specific skills +Skill(commit) +Skill(review-pr *) + +# Deny specific skills +Skill(deploy *) +``` + +Permission syntax: `Skill(name)` for exact match, `Skill(name *)` for prefix match with any arguments. + +**Hide individual skills** by adding `disable-model-invocation: true` to their frontmatter. This removes the skill from Claude's context entirely. + + + The `user-invocable` field only controls menu visibility, not Skill tool access. Use `disable-model-invocation: true` to block programmatic invocation. + + +## Share skills + +Skills can be distributed at different scopes depending on your audience: + +- **Project skills**: Commit `.claude/skills/` to version control +- **Plugins**: Create a `skills/` directory in your [plugin](/en/plugins) +- **Managed**: Deploy organization-wide through [managed settings](/en/permissions#managed-settings) + +### Generate visual output + +Skills can bundle and run scripts in any language, giving Claude capabilities beyond what's possible in a single prompt. One powerful pattern is generating visual output: interactive HTML files that open in your browser for exploring data, debugging, or creating reports. + +This example creates a codebase explorer: an interactive tree view where you can expand and collapse directories, see file sizes at a glance, and identify file types by color. + +Create the Skill directory: + +```bash theme={null} +mkdir -p ~/.claude/skills/codebase-visualizer/scripts +``` + +Create `~/.claude/skills/codebase-visualizer/SKILL.md`. The description tells Claude when to activate this Skill, and the instructions tell Claude to run the bundled script: + +````yaml theme={null} +--- +name: codebase-visualizer +description: Generate an interactive collapsible tree visualization of your codebase. Use when exploring a new repo, understanding project structure, or identifying large files. +allowed-tools: Bash(python *) +--- + +# Codebase Visualizer + +Generate an interactive HTML tree view that shows your project's file structure with collapsible directories. + +## Usage + +Run the visualization script from your project root: + +```bash +python ~/.claude/skills/codebase-visualizer/scripts/visualize.py . +``` + +This creates `codebase-map.html` in the current directory and opens it in your default browser. + +## What the visualization shows + +- **Collapsible directories**: Click folders to expand/collapse +- **File sizes**: Displayed next to each file +- **Colors**: Different colors for different file types +- **Directory totals**: Shows aggregate size of each folder +```` + +Create `~/.claude/skills/codebase-visualizer/scripts/visualize.py`. This script scans a directory tree and generates a self-contained HTML file with: + +- A **summary sidebar** showing file count, directory count, total size, and number of file types +- A **bar chart** breaking down the codebase by file type (top 8 by size) +- A **collapsible tree** where you can expand and collapse directories, with color-coded file type indicators + +The script requires Python but uses only built-in libraries, so there are no packages to install: + +```python expandable theme={null} +#!/usr/bin/env python3 +"""Generate an interactive collapsible tree visualization of a codebase.""" + +import json +import sys +import webbrowser +from pathlib import Path +from collections import Counter + +IGNORE = {'.git', 'node_modules', '__pycache__', '.venv', 'venv', 'dist', 'build'} + +def scan(path: Path, stats: dict) -> dict: + result = {"name": path.name, "children": [], "size": 0} + try: + for item in sorted(path.iterdir()): + if item.name in IGNORE or item.name.startswith('.'): + continue + if item.is_file(): + size = item.stat().st_size + ext = item.suffix.lower() or '(no ext)' + result["children"].append({"name": item.name, "size": size, "ext": ext}) + result["size"] += size + stats["files"] += 1 + stats["extensions"][ext] += 1 + stats["ext_sizes"][ext] += size + elif item.is_dir(): + stats["dirs"] += 1 + child = scan(item, stats) + if child["children"]: + result["children"].append(child) + result["size"] += child["size"] + except PermissionError: + pass + return result + +def generate_html(data: dict, stats: dict, output: Path) -> None: + ext_sizes = stats["ext_sizes"] + total_size = sum(ext_sizes.values()) or 1 + sorted_exts = sorted(ext_sizes.items(), key=lambda x: -x[1])[:8] + colors = { + '.js': '#f7df1e', '.ts': '#3178c6', '.py': '#3776ab', '.go': '#00add8', + '.rs': '#dea584', '.rb': '#cc342d', '.css': '#264de4', '.html': '#e34c26', + '.json': '#6b7280', '.md': '#083fa1', '.yaml': '#cb171e', '.yml': '#cb171e', + '.mdx': '#083fa1', '.tsx': '#3178c6', '.jsx': '#61dafb', '.sh': '#4eaa25', + } + lang_bars = "".join( + f'
{ext}' + f'
' + f'{(size/total_size)*100:.1f}%
' + for ext, size in sorted_exts + ) + def fmt(b): + if b < 1024: return f"{b} B" + if b < 1048576: return f"{b/1024:.1f} KB" + return f"{b/1048576:.1f} MB" + + html = f''' + + Codebase Explorer + + +
+ +
+

📁 {data["name"]}

+
    +
    +
    + +''' + output.write_text(html) + +if __name__ == '__main__': + target = Path(sys.argv[1] if len(sys.argv) > 1 else '.').resolve() + stats = {"files": 0, "dirs": 0, "extensions": Counter(), "ext_sizes": Counter()} + data = scan(target, stats) + out = Path('codebase-map.html') + generate_html(data, stats, out) + print(f'Generated {out.absolute()}') + webbrowser.open(f'file://{out.absolute()}') +``` + +To test, open Claude Code in any project and ask "Visualize this codebase." Claude runs the script, generates `codebase-map.html`, and opens it in your browser. + +This pattern works for any visual output: dependency graphs, test coverage reports, API documentation, or database schema visualizations. The bundled script does the heavy lifting while Claude handles orchestration. + +## Troubleshooting + +### Skill not triggering + +If Claude doesn't use your skill when expected: + +1. Check the description includes keywords users would naturally say +2. Verify the skill appears in `What skills are available?` +3. Try rephrasing your request to match the description more closely +4. Invoke it directly with `/skill-name` if the skill is user-invocable + +### Skill triggers too often + +If Claude uses your skill when you don't want it: + +1. Make the description more specific +2. Add `disable-model-invocation: true` if you only want manual invocation + +### Claude doesn't see all my skills + +Skill descriptions are loaded into context so Claude knows what's available. If you have many skills, they may exceed the character budget. The budget scales dynamically at 2% of the context window, with a fallback of 16,000 characters. Run `/context` to check for a warning about excluded skills. + +To override the limit, set the `SLASH_COMMAND_TOOL_CHAR_BUDGET` environment variable. + +## Related resources + +- **[Subagents](/en/sub-agents)**: delegate tasks to specialized agents +- **[Plugins](/en/plugins)**: package and distribute skills with other extensions +- **[Hooks](/en/hooks)**: automate workflows around tool events +- **[Memory](/en/memory)**: manage CLAUDE.md files for persistent context +- **[Interactive mode](/en/interactive-mode#built-in-commands)**: built-in commands and shortcuts +- **[Permissions](/en/permissions)**: control tool and skill access diff --git a/.claude/docs/common-components/README.md b/.claude/docs/common-components/README.md new file mode 100644 index 000000000..4441289ac --- /dev/null +++ b/.claude/docs/common-components/README.md @@ -0,0 +1,209 @@ +# 공통 컴포넌트 (src/common) + +## 개요 + +`src/common/` 디렉토리에는 프로젝트 전반에서 재사용되는 공통 UI 컴포넌트가 위치한다. +스타일링은 Tailwind CSS + `twMerge`가 주력이며, 일부 MUI 컴포넌트도 사용한다. + +--- + +## 폴더 구조 + +``` +src/common/ +├── alert/ # 알림/경고 모달 +├── badge/ # 배지 +├── box/ # 체크박스, 원형 박스 +├── button/ # 버튼 컴포넌트들 +├── carousel/ # 모바일 캐러셀 +├── container/ # 빈 상태, 에러 상태 컨테이너 +├── dropdown/ # 드롭다운 (필터, FAQ, 옵션) +├── drawer/ # 드로어 관련 +├── header/ # 섹션 헤더, 뒤로가기 헤더 +├── input/ # 입력 필드 (v1, v2) +├── layout/ # 전체 레이아웃, 네비게이션, 푸터 +├── loading/ # 로딩 컨테이너 +├── modal/ # 모달 +├── placeholder/ # 플레이스홀더 +├── price/ # 가격 표시 +├── scroll-to-top/ # 상단 스크롤 버튼 +├── sheet/ # 바텀 시트 +├── table/ # 데이터 테이블 +└── (루트 파일들) # 유틸리티 컴포넌트 +``` + +--- + +## 컴포넌트 분류 + +### UI 기본 컴포넌트 + +| 컴포넌트 | 파일 | 역할 | +|---------|------|------| +| **If** | `If.tsx` | 조건부 렌더링 유틸리티. `condition` prop으로 children 표시 여부 결정 | +| **Break** | `Break.tsx` | 반응형 줄바꿈. breakpoint 이하에서 `
    `, 이상에서 공백 | +| **RequiredStar** | `RequiredStar.tsx` | 필수 항목 표시 빨간 별표 (*) | +| **HorizontalRule** | `HorizontalRule.tsx` | 수평 구분선 | +| **HybridLink** | `HybridLink.tsx` | 내부/외부 링크 통합 처리. 외부 URL은 ``, 내부는 Next.js `` | + +### Badge + +| 컴포넌트 | 파일 | 역할 | +|---------|------|------| +| **Badge** | `badge/Badge.tsx` | 상태 표시 배지 (`success` / `warning` / `error` / `info`) | + +### Button + +| 컴포넌트 | 파일 | 역할 | +|---------|------|------| +| **Button** | `button/Button.tsx` | 기본 버튼 (레거시) | +| **BaseButton** | `button/BaseButton.tsx` | 기본 스타일 버튼. `variant`: filled / outlined / point | +| **SolidButton** | `button/SolidButton.tsx` | 채운 버튼. `variant`: primary / secondary, `size`: xs~xl, `icon` 지원 | +| **OutlinedButton** | `button/OutlinedButton.tsx` | 테두리 버튼 | +| **SelectButton** | `button/SelectButton.tsx` | 선택 버튼 | +| **ModalButton** | `button/ModalButton.tsx` | 모달용 버튼 | +| **ApplyCTA** | `button/ApplyCTA.tsx` | 프로그램 신청 CTA. 마감일/시작일 기반 상태 표시, 카운트다운 포함 | + +### Input / Form + +| 컴포넌트 | 파일 | 역할 | +|---------|------|------| +| **LineInput** | `input/LineInput.tsx` | 라인 스타일 입력 필드. HTML input 속성 그대로 전달 | +| **TextArea** | `input/TextArea.tsx` | 텍스트 영역 입력 | +| **Input v1** | `input/v1/Input.tsx` | 입력 필드 (v1 버전) | +| **Input v2** | `input/v2/Input.tsx` | 입력 필드 (v2 버전) | +| **ControlLabel** | `ControlLabel.tsx` | MUI FormControlLabel 확장. 라디오/체크박스 라벨 (서브텍스트, 오른쪽 슬롯 지원) | + +### Box + +| 컴포넌트 | 파일 | 역할 | +|---------|------|------| +| **CheckBox** | `box/CheckBox.tsx` | 커스텀 체크박스. `checked`, `disabled`, `showCheckIcon` 지원 | +| **CircularBox** | `box/CircularBox.tsx` | 원형 박스 컴포넌트 | + +### Dropdown + +| 컴포넌트 | 파일 | 역할 | +|---------|------|------| +| **FilterDropdown** | `dropdown/FilterDropdown.tsx` | 필터 드롭다운. URL 쿼리 파라미터와 연동, `multiSelect` 지원 | +| **FaqDropdown** | `dropdown/FaqDropdown.tsx` | FAQ 아코디언 드롭다운 | +| **OptionDropdown** | `dropdown/OptionDropdown.tsx` | 일반 옵션 드롭다운 | + +### Header / Section + +| 컴포넌트 | 파일 | 역할 | +|---------|------|------| +| **SectionHeader** | `header/SectionHeader.tsx` | 섹션 제목 헤더 | +| **SectionMainHeader** | `header/SectionMainHeader.tsx` | 섹션 메인 헤더 (큰 제목) | +| **SectionSubHeader** | `header/SectionSubHeader.tsx` | 섹션 서브 헤더 (부제목) | +| **BackHeader** | `header/BackHeader.tsx` | 뒤로가기 버튼 포함 헤더 | +| **MoreHeader** | `header/MoreHeader.tsx` | 더보기 링크 포함 헤더 | +| **Heading2** | `header/Heading2.tsx` | h2 레벨 제목 | + +### Modal & Sheet + +| 컴포넌트 | 파일 | 역할 | +|---------|------|------| +| **BaseModal** | `modal/BaseModal.tsx` | 기본 모달. `isOpen`, `onClose`, `isLoading` 지원 | +| **AlertModal** | `alert/AlertModal.tsx` | 알림 모달. 제목, 확인/취소 버튼, `disabled` 지원 | +| **WarningModal** | `alert/WarningModal.tsx` | 경고 모달 | +| **ReportSubmitModal** | `modal/ReportSubmitModal.tsx` | 리포트 제출 모달 | +| **BaseBottomSheet** | `sheet/BaseBottomSheet.tsx` | 기본 바텀 시트 | +| **BottomSheet** | `sheet/BottomSheeet.tsx` | 바텀 시트 | +| **ModalOverlay** | `ModalOverlay.tsx` | 모달 배경 오버레이. 클릭 시 onClose | +| **ModalPortal** | `ModalPortal.tsx` | createPortal을 사용한 모달 포탈 | + +### Layout + +| 컴포넌트 | 파일 | 역할 | +|---------|------|------| +| **Layout** | `layout/Layout.tsx` | 전체 페이지 레이아웃 (NavBar + children + Footer) | +| **ConditionalLayout** | `layout/ConditionalLayout.tsx` | 조건부 레이아웃 | + +### Navigation (layout/header/) + +| 컴포넌트 | 파일 | 역할 | +|---------|------|------| +| **NavBar** | `layout/header/NavBar.tsx` | 글로벌 네비게이션 바 | +| **GlobalNavItem** | `layout/header/GlobalNavItem.tsx` | 네비 아이템. `active`, `subNavList`, `showDropdownIcon` 지원 | +| **GlobalNavTopBar** | `layout/header/GlobalNavTopBar.tsx` | 네비 상단바 (로고, 로그인, 메뉴 토글) | +| **SideNavContainer** | `layout/header/SideNavContainer.tsx` | 사이드 네비 (모바일 햄버거 메뉴) | +| **SideNavItem** | `layout/header/SideNavItem.tsx` | 사이드 네비 아이템 | +| **SubNavItem** | `layout/header/SubNavItem.tsx` | 서브 네비 아이템 | +| **NavOverlay** | `layout/header/NavOverlay.tsx` | 네비 오버레이 배경 | +| **ExternalNavList** | `layout/header/ExternalNavList.tsx` | 외부 링크 네비 | +| **LoginLink** | `layout/header/LoginLink.tsx` | 로그인 링크 | +| **SignUpLink** | `layout/header/SignUpLink.tsx` | 회원가입 링크 | +| **LogoLink** | `layout/header/LogoLink.tsx` | 로고 링크 | +| **KakaoChannel** | `layout/header/KakaoChannel.tsx` | 카카오 채널 링크 | +| **Promotion** | `layout/header/Promotion.tsx` | 프로모션 배너 | +| **Spacer** | `layout/header/Spacer.tsx` | 네비바 높이만큼 스페이서 | + +### Bottom Navigation + +| 컴포넌트 | 파일 | 역할 | +|---------|------|------| +| **BottomNavBar** | `layout/BottomNavBar.tsx` | 모바일 하단 네비게이션 | +| **BottomNavBarWithPathname** | `layout/BottomNavBarWithPathname.tsx` | 경로 기반 하단 네비게이션 | + +### Footer (layout/footer/) + +| 컴포넌트 | 파일 | 역할 | +|---------|------|------| +| **Footer** | `layout/footer/Footer.tsx` | 전체 푸터 | +| **BottomLinkSection** | `layout/footer/BottomLinkSection.tsx` | 푸터 하단 링크 섹션 | +| **MainLink** | `layout/footer/MainLink.tsx` | 푸터 메인 링크 | +| **DocumentLink** | `layout/footer/DocumentLink.tsx` | 약관/문서 링크 | +| **BusinessInfo** | `layout/footer/BusinessInfo.tsx` | 사업자 정보 | +| **CustomerSupport** | `layout/footer/CustomerSupport.tsx` | 고객 지원 정보 | +| **IconLink** | `layout/footer/IconLink.tsx` | SNS 아이콘 링크 | + +### Container / State + +| 컴포넌트 | 파일 | 역할 | +|---------|------|------| +| **EmptyContainer** | `container/EmptyContainer.tsx` | 데이터 없음 표시. `text` 커스텀 가능 | +| **ErrorContainer** | `container/ErrorContainer.tsx` | 에러 상태 표시 | +| **LoadingContainer** | `loading/LoadingContainer.tsx` | 로딩 상태 표시. `text` 커스텀 가능 | + +### Price + +| 컴포넌트 | 파일 | 역할 | +|---------|------|------| +| **PriceView** | `price/PriceView.tsx` | 가격 표시 (할인율, 원래가/할인가 포함) | +| **PriceSummary** | `price/PriceSummary.tsx` | 가격 요약 | + +### Table + +| 컴포넌트 | 파일 | 역할 | +|---------|------|------| +| **DataTable** | `table/DataTable.tsx` | 데이터 테이블. 체크박스 선택, 행 클릭, 확장 기능 지원 | +| **ExpandableCell** | `table/ExpandableCell.tsx` | 확장 가능한 테이블 셀 | + +### Carousel & Scroll + +| 컴포넌트 | 파일 | 역할 | +|---------|------|------| +| **MobileCarousel** | `carousel/MobileCarousel.tsx` | Swiper 기반 모바일 캐러셀. 제네릭 타입 `items` + `renderItem` 패턴 | +| **ScrollToTop** | `scroll-to-top/ScrollToTop.tsx` | 상단으로 스크롤 버튼 | + +### 기타 유틸리티 + +| 컴포넌트 | 파일 | 역할 | +|---------|------|------| +| **Duration** | `Duration.tsx` | 카운트다운 타이머. deadline 기반 D-day 또는 시:분:초 표시 | +| **ChannelTalkBtn** | `layout/channel/ChannelTalkBtn.tsx` | 채널톡 버튼 | +| **DrawerCloseBtn** | `drawer/DrawerCloseBtn.tsx` | 드로어 닫기 버튼 | +| **PaymentErrorNotification** | `PaymentErrorNotification.tsx` | 결제 오류 알림 | + +--- + +## 주요 패턴 + +- **스타일**: Tailwind CSS + `twMerge()` 클래스 병합 +- **메모이제이션**: `memo()` 활용 성능 최적화 +- **포탈**: 모달/시트를 DOM 최상위로 렌더링 +- **반응형**: 모바일/데스크톱 구분 처리 +- **URL 상태**: FilterDropdown 등에서 쿼리 파라미터 연동 +- **MUI 혼용**: ControlLabel 등 일부 컴포넌트에서 MUI 활용 +- **TypeScript**: 모든 컴포넌트에 Props 타입 정의 diff --git a/.claude/docs/curation-domain/README.md b/.claude/docs/curation-domain/README.md new file mode 100644 index 000000000..660750ffc --- /dev/null +++ b/.claude/docs/curation-domain/README.md @@ -0,0 +1,327 @@ +# 큐레이션 페이지 도메인 구조 + +## 개요 + +큐레이션 페이지는 **사용자 맞춤형 챌린지 추천 시스템**으로, 6가지 페르소나 기반 3단계 질문을 통해 맞춤 프로그램을 추천한다. + +- **사용자 파트**: 페르소나 선택 -> 2단계 질문 -> 맞춤 추천 결과 +- **비교 파트**: 7개 챌린지를 최대 3개까지 선택하여 비교 +- **FAQ 파트**: 4가지 카테고리별 자주 묻는 질문 +- **관리자 파트**: 홈 화면 큐레이션 섹션(배너/리뷰/블로그) CRUD 관리 + +--- + +## 1. 라우트 구조 + +### 사용자 페이지 +``` +/curation -> src/app/(user)/curation/page.tsx +``` + +### 관리자 페이지 +``` +/admin/home/curation -> src/app/admin/home/curation/page.tsx +/admin/home/curation/create -> src/app/admin/home/curation/create/page.tsx +/admin/home/curation/[id]/edit -> src/app/admin/home/curation/[id]/edit/page.tsx +``` + +--- + +## 2. 도메인 파일 구조 + +``` +src/domain/curation/ +├── index.ts # 진입점 (CurationScreen export) +├── types.ts # 모든 타입 정의 +│ +├── screen/ +│ └── CurationScreen.tsx # 메인 화면 (Hero + Form + Comparison + FAQ) +│ +├── hero/ +│ └── CurationHero.tsx # 상단 Hero 배너 +│ +├── nav/ +│ └── CurationStickyNav.tsx # 스티키 네비게이션 바 +│ +├── flow/ # 큐레이션 흐름 (핵심 기능) +│ ├── useCurationFlow.ts # 플로우 전체 상태 관리 훅 +│ ├── curationEngine.ts # 추천 로직 (비즈니스 엔진) +│ ├── curationEngine.test.ts # 추천 로직 테스트 +│ ├── personas.ts # 6가지 페르소나 정의 +│ ├── questions.ts # 페르소나별 질문/선택지 +│ ├── guides.ts # 가이드 콘텐츠 +│ ├── copy.ts # UI 문구 (Hero, Steps) +│ ├── CurationStepper.tsx # 4단계 스텝 표시기 +│ ├── PersonaSelector.tsx # 페르소나 선택 (데스크톱) +│ ├── MobilePersonaSelector.tsx # 페르소나 선택 (모바일) +│ ├── QuestionStep.tsx # 질문 단계 공통 +│ ├── MobileQuestionStep.tsx # 질문 단계 (모바일) +│ ├── ResultSection.tsx # 추천 결과 섹션 +│ ├── DesktopRecommendationCard.tsx # 추천 카드 (데스크톱) +│ └── MobileRecommendationCard.tsx # 추천 카드 (모바일) +│ +├── challenge-comparison/ # 챌린지 비교 섹션 +│ ├── useCompareCart.ts # 비교 장바구니 상태 훅 +│ ├── ChallengeCompareSection.tsx # 비교 섹션 메인 +│ ├── ChallengeCard.tsx # 개별 챌린지 카드 +│ ├── CompareResultCard.tsx # 비교 결과 카드 +│ └── RecommendedComparisons.tsx # 자주 비교하는 조합 +│ +├── faq/ # FAQ 섹션 +│ ├── FaqSection.tsx # FAQ 메인 +│ └── faqs.ts # FAQ 데이터 (4개 카테고리) +│ +└── shared/ # 공유 데이터 + ├── programs.ts # 7개 프로그램 상세 정보 + └── comparisons.ts # 챌린지 비교 데이터 +``` + +### 관리자 도메인 +``` +src/domain/admin/ +├── pages/home/curation/ +│ ├── HomeCurationListPage.tsx # 목록 페이지 (DataGrid) +│ ├── HomeCurationCreatePage.tsx # 생성 페이지 +│ └── HomeCurationEditPage.tsx # 수정 페이지 +└── home/curation/ + ├── CurationItem.tsx # 개별 큐레이션 아이템 + ├── CurationSelectModal.tsx # 아이템 선택 모달 + └── section/ + ├── CurationInfoSection.tsx # 기본 정보 입력 + ├── CurationItemsSection.tsx # 아이템 목록 관리 + └── CurationVisibleSection.tsx # 노출 설정 +``` + +--- + +## 3. 핵심 훅 + +### useCurationFlow + +큐레이션 플로우 전체 상태를 관리하는 핵심 훅. React Hook Form 기반. + +```typescript +const { + formRef, // 폼 영역 ref (스크롤) + currentStep, // 현재 단계 (0~3) + personaId, // 선택된 페르소나 + questionSet, // 페르소나별 질문 세트 + errors, // 폼 검증 에러 + watch, // 폼 값 감시 + setValue, // 폼 값 설정 + goNext, // 다음 단계 + goToStep, // 특정 단계로 이동 (뒤로가기만) + handleRestart, // 처음부터 다시 + result, // 최종 추천 결과 (CurationResult) + scrollToForm, // 폼 영역으로 스크롤 +} = useCurationFlow() +``` + +**플로우:** +1. Step 0: 페르소나 선택 (6가지) +2. Step 1: 질문 1 (상황별 과제) +3. Step 2: 질문 2 (시간/피드백 필요도) +4. Step 3: 추천 결과 (curationEngine 실행) + +### useCompareCart + +챌린지 비교 선택 상태를 관리하는 훅. 최대 3개까지 선택 가능. + +```typescript +const { + cartItems, // 선택된 프로그램 ID 배열 + addToCart, // 프로그램 추가 (최대 3개) + removeFromCart, // 프로그램 제거 + toggleCartItem, // 토글 + clearCart, // 초기화 + isInCart, // 특정 프로그램 선택 여부 + isFull, // 최대 선택 도달 여부 + canCompare, // 비교 가능 여부 (2개 이상) +} = useCompareCart() +``` + +--- + +## 4. 비즈니스 로직 (curationEngine) + +### 추천 엔진 + +``` +입력: FormValues { personaId, step1, step2 } +출력: CurationResult { headline, summary, recommendations[] } +``` + +- 6가지 페르소나 x 2~4가지 질문 조합에 따라 분기 +- 프로그램 우선순위: basic / standard / premium 플랜 추천 +- 중복 제거 로직 포함 + +### 7개 프로그램 + +| ID | 프로그램명 | +|----|-----------| +| experience | 기필코 경험정리 챌린지 | +| resume | 이력서 1주 완성 | +| coverLetter | 자기소개서 2주 완성 | +| portfolio | 포트폴리오 2주 완성 | +| enterpriseCover | 대기업 자기소개서 | +| marketingAllInOne | 마케팅 올인원 | +| hrAllInOne | HR 올인원 | + +### 6가지 페르소나 + +| ID | 타이틀 | +|----|--------| +| starter | 취준 입문 / 경험 정리가 먼저 | +| resume | 이력서부터 빠르게 완성 | +| coverLetter | 자기소개서/지원동기 강화 | +| portfolio | 포트폴리오/직무 자료 준비 | +| specialized | 특화 트랙 (대기업/마케팅/HR) | +| dontKnow | 잘 모르겠어요 | + +--- + +## 5. 타입 시스템 + +### 폼 관련 +```typescript +type PersonaId = 'starter' | 'resume' | 'coverLetter' | 'portfolio' | 'specialized' | 'dontKnow' +type PlanId = 'basic' | 'standard' | 'premium' +type ProgramId = 'experience' | 'resume' | 'coverLetter' | 'portfolio' | 'enterpriseCover' | 'marketingAllInOne' | 'hrAllInOne' + +interface FormValues { + personaId?: PersonaId + step1: string + step2: string +} +``` + +### 결과 관련 +```typescript +interface CurationResult { + personaId: PersonaId + headline: string + summary: string + recommendations: ProgramRecommendation[] + emphasisNotes?: string[] +} + +interface ProgramRecommendation { + programId: ProgramId + emphasis: 'primary' | 'secondary' + reason: string + suggestedPlanId?: PlanId +} +``` + +### 비교 관련 +```typescript +interface ChallengeComparisonRow { + programId: string + label: string + target: string + duration: string + pricing: string + curriculum: string + deliverable: string + feedback: string + features?: string +} + +interface FrequentComparisonItem { + title: string // e.g., "자소서 vs 대기업 자소서" + left: string + right: string + rows: { label: string; left: string; right: string }[] +} +``` + +--- + +## 6. API + +### 파일 위치: `src/api/curation.ts` + +#### 관리자 API +| 훅 | 메서드 | 용도 | +|----|--------|------| +| useGetAdminCurationList | GET | 큐레이션 목록 조회 | +| useGetAdminCurationDetail | GET | 큐레이션 상세 조회 | +| usePostAdminCuration | POST | 큐레이션 생성 | +| usePatchAdminCuration | PATCH | 큐레이션 수정 | +| useDeleteAdminCuration | DELETE | 큐레이션 삭제 | + +#### 사용자 API +| 훅 | 메서드 | 용도 | +|----|--------|------| +| useGetUserCuration | GET | 홈 큐레이션 데이터 조회 (위치별) | + +### 관리자 데이터 스키마 +```typescript +interface CurationListItemType { + curationId: number + locationType: 'UNDER_BANNER' | 'UNDER_REVIEW' | 'UNDER_BLOG' + title: string + startDate: string + endDate: string + isVisible: boolean +} + +interface CurationItemType { + id: number + programType: 'CHALLENGE' | 'LIVE' | 'VOD' | 'REPORT' | 'BLOG' | 'ETC' + programId?: number + reportType?: string + tag?: string + title?: string + url?: string + thumbnail?: string +} +``` + +--- + +## 7. 사용자 화면 구성 + +``` +CurationScreen +├── CurationStickyNav # 스티키 네비게이션 (Form / Comparison / FAQ) +├── CurationHero # Hero 배너 +│ +├── [폼 영역] +│ ├── CurationStepper # 4단계 진행 표시 +│ ├── PersonaSelector # Step 0: 페르소나 선택 +│ ├── QuestionStep # Step 1~2: 질문 답변 +│ └── ResultSection # Step 3: 추천 결과 +│ ├── DesktopRecommendationCard (Primary) +│ └── DesktopRecommendationCard (Secondary) +│ +├── ChallengeCompareSection # 챌린지 비교 섹션 +│ ├── ChallengeCard (x7) # 7개 챌린지 카드 그리드 +│ ├── CompareResultCard # 비교 결과 +│ └── RecommendedComparisons # 자주 비교하는 조합 3가지 +│ +└── FaqSection # FAQ 섹션 (4개 카테고리) +``` + +--- + +## 8. 관리자 화면 기능 + +| 페이지 | 기능 | +|--------|------| +| **List** | 생성된 큐레이션 목록 (위치/제목/기간/노출여부), 개별 노출 토글, 수정/삭제 | +| **Create** | 위치 선택 (BANNER/REVIEW/BLOG), 기본정보, 아이템 추가, 노출설정 | +| **Edit** | 기존 큐레이션 수정 (Create와 동일 구조) | + +--- + +## 9. 설계 특징 + +- **도메인 분리**: 사용자 큐레이션 vs 관리자 큐레이션 명확 분리 +- **컴포넌트 계층**: Screen -> Section -> Card -> UI +- **반응형**: 데스크톱/모바일 별도 컴포넌트 (PersonaSelector, QuestionStep, RecommendationCard) +- **상태 관리**: React Hook Form + 커스텀 훅 +- **정적 데이터**: personas, questions, programs, comparisons, faqs 모두 정적 파일로 관리 +- **API 연동**: React Query + Axios +- **스크롤 최적화**: requestAnimationFrame 활용 +- **SEO**: 메타데이터 설정, canonical URL diff --git a/.claude/docs/tech-stack/README.md b/.claude/docs/tech-stack/README.md new file mode 100644 index 000000000..67ebd1a6b --- /dev/null +++ b/.claude/docs/tech-stack/README.md @@ -0,0 +1,250 @@ +# 기술 스택 및 설정 + +## 런타임 & 언어 + +| 기술 | 버전 | 비고 | +|------|------|------| +| **Node.js** | 20 (`.nvmrc`) | LTS | +| **TypeScript** | ^5.9.3 | strict 모드 | +| **React** | ^18.3.1 | | +| **React DOM** | ^18.3.1 | | + +## 프레임워크 + +| 기술 | 버전 | 비고 | +|------|------|------| +| **Next.js** | ^15.5.7 | App Router, Turbopack 지원 | +| **Sentry** | @sentry/nextjs ^10.32.1 | main/test 브랜치에서만 활성화 | + +## 스타일링 + +| 기술 | 버전 | 비고 | +|------|------|------| +| **Tailwind CSS** | ^3.4.7 | 커스텀 디자인 토큰 (colors, fontSize, borderRadius 등) | +| **PostCSS** | ^8.5.6 | | +| **Autoprefixer** | ^10.4.19 | | +| **tailwind-merge** | ^2.3.0 | `twMerge` 클래스 병합 | +| **tailwind-scrollbar-hide** | ^1.1.7 | 스크롤바 숨김 플러그인 | +| **SASS** | ^1.77.8 | | +| **Emotion (React)** | ^11.11.4 | MUI 내부 사용 | +| **Emotion (Styled)** | ^11.11.5 | | +| **styled-components** | ^6.1.11 | 레거시 일부 사용 | + +## 상태 관리 & 데이터 페칭 + +| 기술 | 버전 | 비고 | +|------|------|------| +| **TanStack React Query** | ^5.49.2 | API 상태 관리 및 캐싱 | +| **React Query Devtools** | ^5.49.2 | 개발용 | +| **Zustand** | ^4.5.4 | 클라이언트 전역 상태 | +| **React Hook Form** | ^7.65.0 | 폼 상태 관리 | +| **@hookform/resolvers** | ^3.3.4 | Zod 연동 | +| **Zod** | ^3.23.8 | 스키마 검증 | +| **Axios** | ^1.12.2 | HTTP 클라이언트 | + +## UI 라이브러리 + +| 기술 | 버전 | 비고 | +|------|------|------| +| **MUI Material** | ^6.4.0 | 관리자 페이지 주로 사용 | +| **MUI X Data Grid** | ^7.24.0 | 관리자 테이블 | +| **MUI X Date Pickers** | ^7.24.0 | 날짜 선택 | +| **Swiper** | ^11.2.4 | 캐러셀/슬라이더 | +| **lucide-react** | ^0.473.0 | 아이콘 | +| **react-icons** | 5.2.1 | 아이콘 (레거시) | + +## 리치 텍스트 에디터 + +| 기술 | 버전 | 비고 | +|------|------|------| +| **Lexical** | ^0.16.1 | 메인 에디터 (관련 패키지 다수) | +| **react-quill** | ^2.0.0 | 레거시 에디터 | +| **KaTeX** | ^0.16.11 | 수식 렌더링 | + +## 애니메이션 & 인터랙션 + +| 기술 | 버전 | 비고 | +|------|------|------| +| **Motion** (Framer Motion) | ^12.23.12 | 애니메이션 | +| **react-wrap-balancer** | ^1.1.1 | 텍스트 밸런싱 | + +## 유틸리티 + +| 기술 | 버전 | 비고 | +|------|------|------| +| **date-fns** | ^4.1.0 | 날짜 유틸 | +| **dayjs** | ^1.11.11 | 날짜 유틸 (레거시 혼용) | +| **es-toolkit** | ^1.40.0 | 유틸리티 함수 | +| **lodash-es** | ^4.17.21 | 유틸리티 함수 (레거시) | +| **classnames** | ^2.5.1 | 클래스 병합 | +| **clsx** | ^2.1.1 | 클래스 병합 | +| **nanoid** | ^5.0.9 | ID 생성 | +| **es-hangul** | ^2.2.3 | 한글 처리 | + +## 결제 + +| 기술 | 버전 | 비고 | +|------|------|------| +| **Toss Payments SDK** | ^2.2.1 | 결제 연동 | + +## 실시간 협업 + +| 기술 | 버전 | 비고 | +|------|------|------| +| **Yjs** | ^13.6.18 | 실시간 협업 CRDT | +| **y-websocket** | ^2.0.4 | WebSocket 프로바이더 | + +## Firebase + +| 기술 | 버전 | 비고 | +|------|------|------| +| **Firebase** | ^10.12.2 | 인증, 푸시 등 | + +## 기타 + +| 기술 | 버전 | 비고 | +|------|------|------| +| **@excalidraw/excalidraw** | ^0.17.6 | 화이트보드 | +| **colorthief** | ^2.6.0 | 이미지 색상 추출 | +| **react-error-boundary** | ^4.0.13 | 에러 바운더리 | +| **react-infinite-scroller** | ^1.2.6 | 무한 스크롤 | +| **react-to-print** | 3.0.5 | 인쇄 기능 | +| **@tanstack/react-table** | ^8.21.3 | 테이블 | + +## 개발 도구 + +| 기술 | 버전 | 비고 | +|------|------|------| +| **Vite** | ^5.4.20 | 테스트용 번들러 | +| **Vitest** | ^1.6.1 | 테스트 프레임워크 | +| **@vitejs/plugin-react** | ^4.3.1 | | +| **@svgr/webpack** | ^8.1.0 | SVG -> React 컴포넌트 변환 | +| **vite-plugin-svgr** | ^4.2.0 | | +| **dotenv** | ^16.4.5 | 환경 변수 | +| **@builder.io/react** | ^8.2.2 | Builder.io CMS 연동 | +| **@builder.io/sdk** | ^6.0.9 | | +| **@vercel/node** | ^3.2.7 | Vercel 서버리스 함수 | + +--- + +## ESLint 설정 + +- **버전**: ESLint ^9 (Flat Config) +- **설정 파일**: `eslint.config.mjs` +- **extends**: `next/core-web-vitals`, `next/typescript` + +### 주요 규칙 + +| 규칙 | 설정 | 비고 | +|------|------|------| +| `@typescript-eslint/no-unused-vars` | warn | 미사용 변수 경고 | +| `@typescript-eslint/no-explicit-any` | warn | any 타입 경고 | +| `no-console` | warn | console 사용 경고 | +| `react/react-in-jsx-scope` | off | React 17+ 자동 import | +| `object-shorthand` | warn (always) | 객체 축약 표현 강제 | +| `no-useless-rename` | warn | 불필요한 rename 경고 | +| `react/jsx-key` | warn (checkFragmentShorthand) | key prop 누락 경고 | +| `react/prop-types` | off | TypeScript 사용으로 비활성화 | +| `@next/next/no-img-element` | off | img 태그 허용 (TODO: 추후 제거) | +| `@typescript-eslint/ban-ts-comment` | warn | ts-comment 경고 | + +### 무시 경로 +``` +.config/*, node_modules/*, .next/*, dist/* +``` + +--- + +## Prettier 설정 + +- **버전**: ^3.3.2 +- **설정 파일**: `.prettierrc` + +| 옵션 | 값 | +|------|------| +| `singleQuote` | true | +| `semi` | true | +| `useTabs` | false | +| `tabWidth` | 2 | +| `trailingComma` | all | +| `printWidth` | 80 | + +### 플러그인 +- **prettier-plugin-tailwindcss** (^0.6.11): Tailwind 클래스 자동 정렬 + - `tailwindFunctions`: `clsx`, `twMerge` 함수 내부도 정렬 대상 + +--- + +## TypeScript 설정 (tsconfig.json) + +| 옵션 | 값 | +|------|------| +| `target` | ES2017 | +| `module` | esnext | +| `moduleResolution` | bundler | +| `strict` | true | +| `jsx` | preserve | +| `incremental` | true | +| `noFallthroughCasesInSwitch` | true | +| `isolatedModules` | true | +| `esModuleInterop` | true | + +### Path Aliases +| alias | 경로 | +|-------|------| +| `@components/*` | `./src/components/*` | +| `@/*` | `./src/*` | +| `@renderer/*` | `./renderer/*` | + +--- + +## Next.js 설정 (next.config.mjs) + +- **Turbopack**: SVG 로더 설정 포함 +- **Webpack**: @svgr/webpack으로 SVG를 React 컴포넌트로 변환 +- **ESLint**: 빌드 시 무시 (`ignoreDuringBuilds: true`) +- **Images remotePatterns**: S3 (letsintern-bucket, letscareer-test-bucket), Builder.io CDN +- **Sentry**: main/test 브랜치에서만 활성화, widenClientFileUpload 사용 + +--- + +## Tailwind CSS 커스텀 디자인 토큰 + +### 색상 시스템 +- **primary**: #4D55F5 (5~90 단계) +- **secondary**: #1BC47D (10~100 단계) +- **tertiary**: #CB81F2 +- **point**: #DAFF7C +- **challenge**: #00A8EB +- **neutral**: #27272D ~ #FAFAFA (0~100 단계) +- **system**: positive-green, positive-blue, error + +### 반응형 breakpoints +| 이름 | 값 | +|------|------| +| xs | 390px | +| sm | 640px | +| md | 768px | +| lg | 991px | +| xl | 1280px | +| 2xl | 1440px | +| 3xl | 1600px | + +### 커스텀 폰트 사이즈 +- xxlarge36 (2.25rem) ~ xxsmall10 (0.625rem) 까지 13단계 + +### 커스텀 borderRadius +- none (0) ~ full (9999px) 까지 12단계 + +--- + +## 스크립트 + +| 명령어 | 설명 | +|--------|------| +| `npm run dev` | 개발 서버 실행 (next dev) | +| `npm run build` | 프로덕션 빌드 (next build) | +| `npm run start` | 프로덕션 서버 실행 (next start) | +| `npm run typecheck` | 타입 체크 (tsc --noEmit) | +| `npm run test` | 테스트 실행 (vitest) | +| `npm run lint` | ESLint 실행 (eslint .) | diff --git a/.claude/hooks/check-tasks.sh b/.claude/hooks/check-tasks.sh new file mode 100644 index 000000000..e52fd9dbb --- /dev/null +++ b/.claude/hooks/check-tasks.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Stop Hook: task-runner 실행 중 Claude 중단 방지 +# .claude/.task-running 파일이 있을 때만 활성화 +# todo/ 에 미완료 작업이 있으면 Claude가 계속 작업하도록 강제 + +INPUT=$(cat) + +# task-runner가 실행 중일 때만 작동 +[ ! -f ".claude/.task-running" ] && exit 0 + +# 무한 루프 방지 (stop_hook_active가 true면 종료 허용) +if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then + exit 0 +fi + +# todo/ 에서 미완료 항목([ ])이 있는 파일 탐색 +REMAINING=$(ls todo/*.md 2>/dev/null | xargs -I{} sh -c 'grep -l "\[ \]" {} 2>/dev/null' 2>/dev/null | head -1) + +if [ -n "$REMAINING" ]; then + # 남은 작업 개수 파악 + COUNT=$(grep -c "\[ \]" "$REMAINING" 2>/dev/null || echo "?") + echo "{\"decision\": \"block\", \"reason\": \"$REMAINING 에 미완료 작업 ${COUNT}개 남음. 모든 [ ] 항목을 [x] 로 완료하기 전까지 계속 실행하세요. 사용자 확인 없이 자율적으로 진행합니다.\"}" +fi + +exit 0 diff --git a/.claude/hooks/inject-task-context.sh b/.claude/hooks/inject-task-context.sh new file mode 100644 index 000000000..b55f56193 --- /dev/null +++ b/.claude/hooks/inject-task-context.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# SessionStart Hook (compact matcher): 컨텍스트 압축 후 task 상태 재주입 +# 컴팩션으로 잃어버린 task 진행 상황을 Claude에게 다시 알려줌 + +# todo/ 에 진행 중인 task 파일이 있는지 확인 +TASK_FILES=$(ls todo/*.md 2>/dev/null) + +if [ -z "$TASK_FILES" ]; then + exit 0 +fi + +echo "=== 컨텍스트 재주입: 진행 중인 Task ===" +echo "" + +for FILE in $TASK_FILES; do + REMAINING=$(grep -c "\[ \]" "$FILE" 2>/dev/null || echo 0) + DONE=$(grep -c "\[x\]" "$FILE" 2>/dev/null || echo 0) + + if [ "$REMAINING" -gt 0 ]; then + echo "📋 파일: $FILE" + echo " 완료: ${DONE}개 / 남은 작업: ${REMAINING}개" + echo " 다음 작업:" + grep "\[ \]" "$FILE" | head -3 | sed 's/^/ /' + echo "" + fi +done + +# .task-running 있으면 알림 +if [ -f ".claude/.task-running" ]; then + echo "⚠️ task-runner가 실행 중입니다. 중단 없이 계속 진행하세요." +fi + +echo "=== End ===" diff --git a/.claude/hooks/post-edit-lint.sh b/.claude/hooks/post-edit-lint.sh new file mode 100644 index 000000000..d7cb3e5be --- /dev/null +++ b/.claude/hooks/post-edit-lint.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# PostToolUse Hook: ESLint + Prettier 자동 실행 +# Claude가 Edit/Write로 파일 수정 시 자동 실행 (jq 방식 - 공식 문서 권장) + +INPUT=$(cat) +FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') + +# 파일 경로 없으면 종료 +[ -z "$FILE_PATH" ] && exit 0 + +# 파일 존재 여부 확인 +[ ! -f "$FILE_PATH" ] && exit 0 + +# JS/TS 파일만 처리 +case "$FILE_PATH" in + *.ts|*.tsx|*.js|*.jsx) ;; + *) exit 0 ;; +esac + +# ESLint 자동 수정 +npx eslint --fix "$FILE_PATH" 2>/dev/null || true + +# Prettier 포맷팅 +npx prettier --write "$FILE_PATH" 2>/dev/null || true diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 000000000..48afe62a7 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "next-dev", + "runtimeExecutable": "node", + "runtimeArgs": ["node_modules/next/dist/bin/next", "dev"], + "port": 3000 + }, + { + "name": "next-start", + "runtimeExecutable": "node", + "runtimeArgs": ["node_modules/next/dist/bin/next", "start"], + "port": 3000 + } + ] +} diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..02f86166a --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,36 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "bash .claude/hooks/post-edit-lint.sh" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "bash .claude/hooks/check-tasks.sh" + } + ] + } + ], + "SessionStart": [ + { + "matcher": "compact", + "hooks": [ + { + "type": "command", + "command": "bash .claude/hooks/inject-task-context.sh" + } + ] + } + ] + } +} diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 3f582e9a1..c39835522 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,13 @@ { "permissions": { "allow": [ - "Bash(npx tsc:*)" + "Bash(*)", + "mcp__plugin_figma_figma-desktop__get_design_context", + "mcp__plugin_figma_figma-desktop__get_screenshot", + "mcp__plugin_figma_figma-desktop__get_metadata", + "mcp__Claude_Preview__preview_start", + "WebFetch(domain:www.figma.com)", + "WebFetch(domain:api.figma.com)" ] } } diff --git a/.claude/skills/task-maker/SKILL.md b/.claude/skills/task-maker/SKILL.md new file mode 100644 index 000000000..0e9ead897 --- /dev/null +++ b/.claude/skills/task-maker/SKILL.md @@ -0,0 +1,98 @@ +--- +name: task-maker +description: "PRD나 요구사항을 기반으로 Git 워크플로우에 맞는 작업 목록을 생성합니다. 사용자가 'PRD 기반 작업 만들어줘', '작업 목록 생성', 'task 만들어줘', '기능 분해해줘' 등을 요청할 때 사용합니다." +argument-hint: "[prd-file-path]" +disable-model-invocation: true +allowed-tools: Read, Write, Glob +--- + +# PRD 기반 작업 목록 생성 + +PRD를 분석하여 **Push 단위 파일 분리** + **커밋 단위 하위 작업** 구조로 task 파일을 생성합니다. + +--- + +## 폴더 구조 + +``` +todo/ ← PRD 및 신규 Task 파일 + prd-*.md + tasks-[prd-name]-push1.md + tasks-[prd-name]-push2.md + +done/ ← 완료된 Task + 결과보고서 + tasks-[prd-name]-push1.md + result-[prd-name]-push1.md +``` + +`todo/`, `done/` 없으면 자동 생성. + +--- + +## 생성 프로세스 (확인 없이 자동 완료) + +### 1. PRD 분석 +- 인자로 받은 파일 읽기 (없으면 `todo/` 에서 최신 prd-*.md 탐색) +- 기능 요구사항 전체 파악 + +### 2. Push 단위 파일 분리 + +각 Push = **별도 파일** (`todo/tasks-[prd-name]-push[N].md`): +- 독립 배포 가능한 기능 단위 +- 하위 커밋 3~7개 + +### 3. 커밋 단위 하위 작업 + +각 하위 작업 = **하나의 Git 커밋**: +- 파일 수정 5개 미만 +- 독립 테스트 가능 + +### 4. 테스트 작업 자동 추가 + +모든 구현 작업 뒤에 자동 추가: +- `[N].T1` 테스트 코드 작성 +- `[N].T2` 테스트 실행 및 검증 + +--- + +## 출력 형식 (Push 파일) + +```markdown +# Tasks: [PRD명] - Push [N] + +> PRD: `todo/[prd-파일명].md` +> Push 범위: [기능 요약] +> 상태: 🔲 진행 중 + +--- + +### 관련 파일 + +- `src/components/Foo.tsx` - 주요 컴포넌트 + +--- + +## 작업 + +- [ ] 1.0 상위 작업 (Push 범위) + - [ ] 1.1 하위 작업 (커밋 단위) + - [ ] 1.1.T1 테스트 코드 작성 + - [ ] 1.1.T2 테스트 실행 및 검증 + - [ ] 1.2 하위 작업 (커밋 단위) + - [ ] 1.2.T1 테스트 코드 작성 + - [ ] 1.2.T2 테스트 실행 및 검증 +``` + +--- + +## 규칙 + +- 사용자 확인 없이 분석 → 파일 저장까지 자동 완료 +- 반드시 Push 단위로 파일 분리 (단일 파일 금지) +- React/TSX 관련 작업에는 Vercel 베스트 프랙티스 반영 +- 완료 후 생성된 파일 목록만 간단히 보고 + +## 참고 자료 + +- 상세 규칙: [rules/](../vercel-react-best-practices/rules/) +- 전체 가이드: [AGENTS.md](../vercel-react-best-practices/AGENTS.md) diff --git a/.claude/skills/task-runner/SKILL.md b/.claude/skills/task-runner/SKILL.md new file mode 100644 index 000000000..0f73fa18b --- /dev/null +++ b/.claude/skills/task-runner/SKILL.md @@ -0,0 +1,116 @@ +--- +name: task-runner +description: "todo/ 폴더의 task 파일을 읽고 task-executor 에이전트에 위임하여 자동 실행합니다. 사용자가 '작업 실행', '태스크 실행', '다음 작업', '작업 계속', '이어서 진행' 등을 요청할 때 사용합니다." +argument-hint: "[task-file-path]" +disable-model-invocation: true +allowed-tools: Read, Write, Bash, Glob, Task +--- + +# Task Runner — 오케스트레이터 + +`todo/` 의 task 파일을 읽고 `task-executor` 에이전트에 실행을 위임합니다. +실제 코드 작성/테스트/커밋은 에이전트가 담당합니다. + +--- + +## 시작 절차 + +```bash +# 1. sentinel 파일 생성 (Stop 훅이 이걸 보고 중단 방지) +touch .claude/.task-running + +# 2. done/ 디렉토리 확인 +mkdir -p done +``` + +파일이 지정되지 않은 경우 `todo/` 의 `tasks-*.md` 목록을 보여주고 선택 요청 +(이 경우에만 사용자에게 질문 허용 — 이후는 완전 자율) + +--- + +## 실행 루프 + +``` +todo/ 에서 미완료 task 파일 로드 + ↓ +task-executor 에이전트에 위임 (전체 파일 내용 + 지시사항 전달) + ↓ +에이전트 완료 보고 수신 + ↓ +Push 파일 완료 확인 → done/ 이동 + 결과보고서 작성 + ↓ +다음 todo/ 파일 있으면 → 자동으로 계속 (질문 금지) + ↓ +모든 파일 완료 → sentinel 삭제 → 최종 보고 +``` + +--- + +## 에이전트 위임 방법 + +각 Push 파일마다 `task-executor` 에이전트에 위임: + +``` +task-executor 에이전트를 사용하여 다음 task 파일을 실행하세요: + +파일: todo/tasks-[name]-push[N].md +내용: [파일 전체 내용] + +지시사항: +- 모든 미완료([ ]) 작업을 순서대로 실행 +- 각 하위 작업 완료 시 즉시 커밋 +- 모든 작업 완료 시 git push 수행 +- 완료된 항목은 [x] 로 체크 +- 오류 시 T3 수정 작업 추가 후 자동 해결 +``` + +--- + +## Push 파일 완료 처리 + +모든 항목이 [x] 된 후: + +```bash +# 파일을 done/ 으로 이동 +mv todo/tasks-[name]-pushN.md done/ + +# 결과보고서 생성 (done/result-[name]-pushN.md) +``` + +결과보고서 형식: +```markdown +# 결과보고서: [파일명] + +> 완료일: [날짜] +> Push 범위: [기능 요약] + +## 구현 요약 + +| 작업 | 상태 | 커밋 | +|---|---|---| +| 1.1 [작업명] | ✅ | `해시` | + +## 생성/수정 파일 + +- `src/...` - [변경 내용] + +## 테스트 결과 + +- 통과: N개 + +## 이슈 및 특이사항 + +- [발생한 오류 및 해결법] +- [적용한 Vercel 규칙] +``` + +--- + +## 종료 처리 + +```bash +# 모든 Push 완료 시 sentinel 삭제 +rm -f .claude/.task-running +``` + +이후 Stop 훅이 중단을 허용하고, 최종 보고 후 종료. diff --git a/.claude/skills/vercel-react-best-practices/AGENTS.md b/.claude/skills/vercel-react-best-practices/AGENTS.md new file mode 100644 index 000000000..f9b9e99c4 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices/AGENTS.md @@ -0,0 +1,2410 @@ +# React Best Practices + +**Version 1.0.0** +Vercel Engineering +January 2026 + +> **Note:** +> This document is mainly for agents and LLMs to follow when maintaining, +> generating, or refactoring React and Next.js codebases at Vercel. Humans +> may also find it useful, but guidance here is optimized for automation +> and consistency by AI-assisted workflows. + +--- + +## Abstract + +Comprehensive performance optimization guide for React and Next.js applications, designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation. + +--- + +## Table of Contents + +1. [Eliminating Waterfalls](#1-eliminating-waterfalls) — **CRITICAL** + - 1.1 [Defer Await Until Needed](#11-defer-await-until-needed) + - 1.2 [Dependency-Based Parallelization](#12-dependency-based-parallelization) + - 1.3 [Prevent Waterfall Chains in API Routes](#13-prevent-waterfall-chains-in-api-routes) + - 1.4 [Promise.all() for Independent Operations](#14-promiseall-for-independent-operations) + - 1.5 [Strategic Suspense Boundaries](#15-strategic-suspense-boundaries) +2. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL** + - 2.1 [Avoid Barrel File Imports](#21-avoid-barrel-file-imports) + - 2.2 [Conditional Module Loading](#22-conditional-module-loading) + - 2.3 [Defer Non-Critical Third-Party Libraries](#23-defer-non-critical-third-party-libraries) + - 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components) + - 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent) +3. [Server-Side Performance](#3-server-side-performance) — **HIGH** + - 3.1 [Cross-Request LRU Caching](#31-cross-request-lru-caching) + - 3.2 [Minimize Serialization at RSC Boundaries](#32-minimize-serialization-at-rsc-boundaries) + - 3.3 [Parallel Data Fetching with Component Composition](#33-parallel-data-fetching-with-component-composition) + - 3.4 [Per-Request Deduplication with React.cache()](#34-per-request-deduplication-with-reactcache) + - 3.5 [Use after() for Non-Blocking Operations](#35-use-after-for-non-blocking-operations) +4. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH** + - 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners) + - 4.2 [Use Passive Event Listeners for Scrolling Performance](#42-use-passive-event-listeners-for-scrolling-performance) + - 4.3 [Use SWR for Automatic Deduplication](#43-use-swr-for-automatic-deduplication) + - 4.4 [Version and Minimize localStorage Data](#44-version-and-minimize-localstorage-data) +5. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM** + - 5.1 [Defer State Reads to Usage Point](#51-defer-state-reads-to-usage-point) + - 5.2 [Extract to Memoized Components](#52-extract-to-memoized-components) + - 5.3 [Narrow Effect Dependencies](#53-narrow-effect-dependencies) + - 5.4 [Subscribe to Derived State](#54-subscribe-to-derived-state) + - 5.5 [Use Functional setState Updates](#55-use-functional-setstate-updates) + - 5.6 [Use Lazy State Initialization](#56-use-lazy-state-initialization) + - 5.7 [Use Transitions for Non-Urgent Updates](#57-use-transitions-for-non-urgent-updates) +6. [Rendering Performance](#6-rendering-performance) — **MEDIUM** + - 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element) + - 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists) + - 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements) + - 6.4 [Optimize SVG Precision](#64-optimize-svg-precision) + - 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering) + - 6.6 [Use Activity Component for Show/Hide](#66-use-activity-component-for-showhide) + - 6.7 [Use Explicit Conditional Rendering](#67-use-explicit-conditional-rendering) +7. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM** + - 7.1 [Batch DOM CSS Changes](#71-batch-dom-css-changes) + - 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups) + - 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops) + - 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls) + - 7.5 [Cache Storage API Calls](#75-cache-storage-api-calls) + - 7.6 [Combine Multiple Array Iterations](#76-combine-multiple-array-iterations) + - 7.7 [Early Length Check for Array Comparisons](#77-early-length-check-for-array-comparisons) + - 7.8 [Early Return from Functions](#78-early-return-from-functions) + - 7.9 [Hoist RegExp Creation](#79-hoist-regexp-creation) + - 7.10 [Use Loop for Min/Max Instead of Sort](#710-use-loop-for-minmax-instead-of-sort) + - 7.11 [Use Set/Map for O(1) Lookups](#711-use-setmap-for-o1-lookups) + - 7.12 [Use toSorted() Instead of sort() for Immutability](#712-use-tosorted-instead-of-sort-for-immutability) +8. [Advanced Patterns](#8-advanced-patterns) — **LOW** + - 8.1 [Store Event Handlers in Refs](#81-store-event-handlers-in-refs) + - 8.2 [useLatest for Stable Callback Refs](#82-uselatest-for-stable-callback-refs) + +--- + +## 1. Eliminating Waterfalls + +**Impact: CRITICAL** + +Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains. + +### 1.1 Defer Await Until Needed + +**Impact: HIGH (avoids blocking unused code paths)** + +Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them. + +**Incorrect: blocks both branches** + +```typescript +async function handleRequest(userId: string, skipProcessing: boolean) { + const userData = await fetchUserData(userId) + + if (skipProcessing) { + // Returns immediately but still waited for userData + return { skipped: true } + } + + // Only this branch uses userData + return processUserData(userData) +} +``` + +**Correct: only blocks when needed** + +```typescript +async function handleRequest(userId: string, skipProcessing: boolean) { + if (skipProcessing) { + // Returns immediately without waiting + return { skipped: true } + } + + // Fetch only when needed + const userData = await fetchUserData(userId) + return processUserData(userData) +} +``` + +**Another example: early return optimization** + +```typescript +// Incorrect: always fetches permissions +async function updateResource(resourceId: string, userId: string) { + const permissions = await fetchPermissions(userId) + const resource = await getResource(resourceId) + + if (!resource) { + return { error: 'Not found' } + } + + if (!permissions.canEdit) { + return { error: 'Forbidden' } + } + + return await updateResourceData(resource, permissions) +} + +// Correct: fetches only when needed +async function updateResource(resourceId: string, userId: string) { + const resource = await getResource(resourceId) + + if (!resource) { + return { error: 'Not found' } + } + + const permissions = await fetchPermissions(userId) + + if (!permissions.canEdit) { + return { error: 'Forbidden' } + } + + return await updateResourceData(resource, permissions) +} +``` + +This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive. + +### 1.2 Dependency-Based Parallelization + +**Impact: CRITICAL (2-10× improvement)** + +For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment. + +**Incorrect: profile waits for config unnecessarily** + +```typescript +const [user, config] = await Promise.all([ + fetchUser(), + fetchConfig() +]) +const profile = await fetchProfile(user.id) +``` + +**Correct: config and profile run in parallel** + +```typescript +import { all } from 'better-all' + +const { user, config, profile } = await all({ + async user() { return fetchUser() }, + async config() { return fetchConfig() }, + async profile() { + return fetchProfile((await this.$.user).id) + } +}) +``` + +Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all) + +### 1.3 Prevent Waterfall Chains in API Routes + +**Impact: CRITICAL (2-10× improvement)** + +In API routes and Server Actions, start independent operations immediately, even if you don't await them yet. + +**Incorrect: config waits for auth, data waits for both** + +```typescript +export async function GET(request: Request) { + const session = await auth() + const config = await fetchConfig() + const data = await fetchData(session.user.id) + return Response.json({ data, config }) +} +``` + +**Correct: auth and config start immediately** + +```typescript +export async function GET(request: Request) { + const sessionPromise = auth() + const configPromise = fetchConfig() + const session = await sessionPromise + const [config, data] = await Promise.all([ + configPromise, + fetchData(session.user.id) + ]) + return Response.json({ data, config }) +} +``` + +For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization). + +### 1.4 Promise.all() for Independent Operations + +**Impact: CRITICAL (2-10× improvement)** + +When async operations have no interdependencies, execute them concurrently using `Promise.all()`. + +**Incorrect: sequential execution, 3 round trips** + +```typescript +const user = await fetchUser() +const posts = await fetchPosts() +const comments = await fetchComments() +``` + +**Correct: parallel execution, 1 round trip** + +```typescript +const [user, posts, comments] = await Promise.all([ + fetchUser(), + fetchPosts(), + fetchComments() +]) +``` + +### 1.5 Strategic Suspense Boundaries + +**Impact: HIGH (faster initial paint)** + +Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads. + +**Incorrect: wrapper blocked by data fetching** + +```tsx +async function Page() { + const data = await fetchData() // Blocks entire page + + return ( +
    +
    Sidebar
    +
    Header
    +
    + +
    +
    Footer
    +
    + ) +} +``` + +The entire layout waits for data even though only the middle section needs it. + +**Correct: wrapper shows immediately, data streams in** + +```tsx +function Page() { + return ( +
    +
    Sidebar
    +
    Header
    +
    + }> + + +
    +
    Footer
    +
    + ) +} + +async function DataDisplay() { + const data = await fetchData() // Only blocks this component + return
    {data.content}
    +} +``` + +Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data. + +**Alternative: share promise across components** + +```tsx +function Page() { + // Start fetch immediately, but don't await + const dataPromise = fetchData() + + return ( +
    +
    Sidebar
    +
    Header
    + }> + + + +
    Footer
    +
    + ) +} + +function DataDisplay({ dataPromise }: { dataPromise: Promise }) { + const data = use(dataPromise) // Unwraps the promise + return
    {data.content}
    +} + +function DataSummary({ dataPromise }: { dataPromise: Promise }) { + const data = use(dataPromise) // Reuses the same promise + return
    {data.summary}
    +} +``` + +Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together. + +**When NOT to use this pattern:** + +- Critical data needed for layout decisions (affects positioning) + +- SEO-critical content above the fold + +- Small, fast queries where suspense overhead isn't worth it + +- When you want to avoid layout shift (loading → content jump) + +**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities. + +--- + +## 2. Bundle Size Optimization + +**Impact: CRITICAL** + +Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint. + +### 2.1 Avoid Barrel File Imports + +**Impact: CRITICAL (200-800ms import cost, slow builds)** + +Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`). + +Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts. + +**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph. + +**Incorrect: imports entire library** + +```tsx +import { Check, X, Menu } from 'lucide-react' +// Loads 1,583 modules, takes ~2.8s extra in dev +// Runtime cost: 200-800ms on every cold start + +import { Button, TextField } from '@mui/material' +// Loads 2,225 modules, takes ~4.2s extra in dev +``` + +**Correct: imports only what you need** + +```tsx +import Check from 'lucide-react/dist/esm/icons/check' +import X from 'lucide-react/dist/esm/icons/x' +import Menu from 'lucide-react/dist/esm/icons/menu' +// Loads only 3 modules (~2KB vs ~1MB) + +import Button from '@mui/material/Button' +import TextField from '@mui/material/TextField' +// Loads only what you use +``` + +**Alternative: Next.js 13.5+** + +```js +// next.config.js - use optimizePackageImports +module.exports = { + experimental: { + optimizePackageImports: ['lucide-react', '@mui/material'] + } +} + +// Then you can keep the ergonomic barrel imports: +import { Check, X, Menu } from 'lucide-react' +// Automatically transformed to direct imports at build time +``` + +Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR. + +Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`. + +Reference: [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js) + +### 2.2 Conditional Module Loading + +**Impact: HIGH (loads large data only when needed)** + +Load large data or modules only when a feature is activated. + +**Example: lazy-load animation frames** + +```tsx +function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch> }) { + const [frames, setFrames] = useState(null) + + useEffect(() => { + if (enabled && !frames && typeof window !== 'undefined') { + import('./animation-frames.js') + .then(mod => setFrames(mod.frames)) + .catch(() => setEnabled(false)) + } + }, [enabled, frames, setEnabled]) + + if (!frames) return + return +} +``` + +The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed. + +### 2.3 Defer Non-Critical Third-Party Libraries + +**Impact: MEDIUM (loads after hydration)** + +Analytics, logging, and error tracking don't block user interaction. Load them after hydration. + +**Incorrect: blocks initial bundle** + +```tsx +import { Analytics } from '@vercel/analytics/react' + +export default function RootLayout({ children }) { + return ( + + + {children} + + + + ) +} +``` + +**Correct: loads after hydration** + +```tsx +import dynamic from 'next/dynamic' + +const Analytics = dynamic( + () => import('@vercel/analytics/react').then(m => m.Analytics), + { ssr: false } +) + +export default function RootLayout({ children }) { + return ( + + + {children} + + + + ) +} +``` + +### 2.4 Dynamic Imports for Heavy Components + +**Impact: CRITICAL (directly affects TTI and LCP)** + +Use `next/dynamic` to lazy-load large components not needed on initial render. + +**Incorrect: Monaco bundles with main chunk ~300KB** + +```tsx +import { MonacoEditor } from './monaco-editor' + +function CodePanel({ code }: { code: string }) { + return +} +``` + +**Correct: Monaco loads on demand** + +```tsx +import dynamic from 'next/dynamic' + +const MonacoEditor = dynamic( + () => import('./monaco-editor').then(m => m.MonacoEditor), + { ssr: false } +) + +function CodePanel({ code }: { code: string }) { + return +} +``` + +### 2.5 Preload Based on User Intent + +**Impact: MEDIUM (reduces perceived latency)** + +Preload heavy bundles before they're needed to reduce perceived latency. + +**Example: preload on hover/focus** + +```tsx +function EditorButton({ onClick }: { onClick: () => void }) { + const preload = () => { + if (typeof window !== 'undefined') { + void import('./monaco-editor') + } + } + + return ( + + ) +} +``` + +**Example: preload when feature flag is enabled** + +```tsx +function FlagsProvider({ children, flags }: Props) { + useEffect(() => { + if (flags.editorEnabled && typeof window !== 'undefined') { + void import('./monaco-editor').then(mod => mod.init()) + } + }, [flags.editorEnabled]) + + return + {children} + +} +``` + +The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed. + +--- + +## 3. Server-Side Performance + +**Impact: HIGH** + +Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times. + +### 3.1 Cross-Request LRU Caching + +**Impact: HIGH (caches across requests)** + +`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache. + +**Implementation:** + +```typescript +import { LRUCache } from 'lru-cache' + +const cache = new LRUCache({ + max: 1000, + ttl: 5 * 60 * 1000 // 5 minutes +}) + +export async function getUser(id: string) { + const cached = cache.get(id) + if (cached) return cached + + const user = await db.user.findUnique({ where: { id } }) + cache.set(id, user) + return user +} + +// Request 1: DB query, result cached +// Request 2: cache hit, no DB query +``` + +Use when sequential user actions hit multiple endpoints needing the same data within seconds. + +**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis. + +**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching. + +Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache) + +### 3.2 Minimize Serialization at RSC Boundaries + +**Impact: HIGH (reduces data transfer size)** + +The React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so **size matters a lot**. Only pass fields that the client actually uses. + +**Incorrect: serializes all 50 fields** + +```tsx +async function Page() { + const user = await fetchUser() // 50 fields + return +} + +'use client' +function Profile({ user }: { user: User }) { + return
    {user.name}
    // uses 1 field +} +``` + +**Correct: serializes only 1 field** + +```tsx +async function Page() { + const user = await fetchUser() + return +} + +'use client' +function Profile({ name }: { name: string }) { + return
    {name}
    +} +``` + +### 3.3 Parallel Data Fetching with Component Composition + +**Impact: CRITICAL (eliminates server-side waterfalls)** + +React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching. + +**Incorrect: Sidebar waits for Page's fetch to complete** + +```tsx +export default async function Page() { + const header = await fetchHeader() + return ( +
    +
    {header}
    + +
    + ) +} + +async function Sidebar() { + const items = await fetchSidebarItems() + return +} +``` + +**Correct: both fetch simultaneously** + +```tsx +async function Header() { + const data = await fetchHeader() + return
    {data}
    +} + +async function Sidebar() { + const items = await fetchSidebarItems() + return +} + +export default function Page() { + return ( +
    +
    + +
    + ) +} +``` + +**Alternative with children prop:** + +```tsx +async function Header() { + const data = await fetchHeader() + return
    {data}
    +} + +async function Sidebar() { + const items = await fetchSidebarItems() + return +} + +function Layout({ children }: { children: ReactNode }) { + return ( +
    +
    + {children} +
    + ) +} + +export default function Page() { + return ( + + + + ) +} +``` + +### 3.4 Per-Request Deduplication with React.cache() + +**Impact: MEDIUM (deduplicates within request)** + +Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most. + +**Usage:** + +```typescript +import { cache } from 'react' + +export const getCurrentUser = cache(async () => { + const session = await auth() + if (!session?.user?.id) return null + return await db.user.findUnique({ + where: { id: session.user.id } + }) +}) +``` + +Within a single request, multiple calls to `getCurrentUser()` execute the query only once. + +**Avoid inline objects as arguments:** + +`React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits. + +**Incorrect: always cache miss** + +```typescript +const getUser = cache(async (params: { uid: number }) => { + return await db.user.findUnique({ where: { id: params.uid } }) +}) + +// Each call creates new object, never hits cache +getUser({ uid: 1 }) +getUser({ uid: 1 }) // Cache miss, runs query again +``` + +**Correct: cache hit** + +```typescript +const params = { uid: 1 } +getUser(params) // Query runs +getUser(params) // Cache hit (same reference) +``` + +If you must pass objects, pass the same reference: + +**Next.js-Specific Note:** + +In Next.js, the `fetch` API is automatically extended with request memoization. Requests with the same URL and options are automatically deduplicated within a single request, so you don't need `React.cache()` for `fetch` calls. However, `React.cache()` is still essential for other async tasks: + +- Database queries (Prisma, Drizzle, etc.) + +- Heavy computations + +- Authentication checks + +- File system operations + +- Any non-fetch async work + +Use `React.cache()` to deduplicate these operations across your component tree. + +Reference: [https://react.dev/reference/react/cache](https://react.dev/reference/react/cache) + +### 3.5 Use after() for Non-Blocking Operations + +**Impact: MEDIUM (faster response times)** + +Use Next.js's `after()` to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response. + +**Incorrect: blocks response** + +```tsx +import { logUserAction } from '@/app/utils' + +export async function POST(request: Request) { + // Perform mutation + await updateDatabase(request) + + // Logging blocks the response + const userAgent = request.headers.get('user-agent') || 'unknown' + await logUserAction({ userAgent }) + + return new Response(JSON.stringify({ status: 'success' }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }) +} +``` + +**Correct: non-blocking** + +```tsx +import { after } from 'next/server' +import { headers, cookies } from 'next/headers' +import { logUserAction } from '@/app/utils' + +export async function POST(request: Request) { + // Perform mutation + await updateDatabase(request) + + // Log after response is sent + after(async () => { + const userAgent = (await headers()).get('user-agent') || 'unknown' + const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous' + + logUserAction({ sessionCookie, userAgent }) + }) + + return new Response(JSON.stringify({ status: 'success' }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }) +} +``` + +The response is sent immediately while logging happens in the background. + +**Common use cases:** + +- Analytics tracking + +- Audit logging + +- Sending notifications + +- Cache invalidation + +- Cleanup tasks + +**Important notes:** + +- `after()` runs even if the response fails or redirects + +- Works in Server Actions, Route Handlers, and Server Components + +Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after) + +--- + +## 4. Client-Side Data Fetching + +**Impact: MEDIUM-HIGH** + +Automatic deduplication and efficient data fetching patterns reduce redundant network requests. + +### 4.1 Deduplicate Global Event Listeners + +**Impact: LOW (single listener for N components)** + +Use `useSWRSubscription()` to share global event listeners across component instances. + +**Incorrect: N instances = N listeners** + +```tsx +function useKeyboardShortcut(key: string, callback: () => void) { + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.metaKey && e.key === key) { + callback() + } + } + window.addEventListener('keydown', handler) + return () => window.removeEventListener('keydown', handler) + }, [key, callback]) +} +``` + +When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener. + +**Correct: N instances = 1 listener** + +```tsx +import useSWRSubscription from 'swr/subscription' + +// Module-level Map to track callbacks per key +const keyCallbacks = new Map void>>() + +function useKeyboardShortcut(key: string, callback: () => void) { + // Register this callback in the Map + useEffect(() => { + if (!keyCallbacks.has(key)) { + keyCallbacks.set(key, new Set()) + } + keyCallbacks.get(key)!.add(callback) + + return () => { + const set = keyCallbacks.get(key) + if (set) { + set.delete(callback) + if (set.size === 0) { + keyCallbacks.delete(key) + } + } + } + }, [key, callback]) + + useSWRSubscription('global-keydown', () => { + const handler = (e: KeyboardEvent) => { + if (e.metaKey && keyCallbacks.has(e.key)) { + keyCallbacks.get(e.key)!.forEach(cb => cb()) + } + } + window.addEventListener('keydown', handler) + return () => window.removeEventListener('keydown', handler) + }) +} + +function Profile() { + // Multiple shortcuts will share the same listener + useKeyboardShortcut('p', () => { /* ... */ }) + useKeyboardShortcut('k', () => { /* ... */ }) + // ... +} +``` + +### 4.2 Use Passive Event Listeners for Scrolling Performance + +**Impact: MEDIUM (eliminates scroll delay caused by event listeners)** + +Add `{ passive: true }` to touch and wheel event listeners to enable immediate scrolling. Browsers normally wait for listeners to finish to check if `preventDefault()` is called, causing scroll delay. + +**Incorrect:** + +```typescript +useEffect(() => { + const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX) + const handleWheel = (e: WheelEvent) => console.log(e.deltaY) + + document.addEventListener('touchstart', handleTouch) + document.addEventListener('wheel', handleWheel) + + return () => { + document.removeEventListener('touchstart', handleTouch) + document.removeEventListener('wheel', handleWheel) + } +}, []) +``` + +**Correct:** + +```typescript +useEffect(() => { + const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX) + const handleWheel = (e: WheelEvent) => console.log(e.deltaY) + + document.addEventListener('touchstart', handleTouch, { passive: true }) + document.addEventListener('wheel', handleWheel, { passive: true }) + + return () => { + document.removeEventListener('touchstart', handleTouch) + document.removeEventListener('wheel', handleWheel) + } +}, []) +``` + +**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`. + +**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`. + +### 4.3 Use SWR for Automatic Deduplication + +**Impact: MEDIUM-HIGH (automatic deduplication)** + +SWR enables request deduplication, caching, and revalidation across component instances. + +**Incorrect: no deduplication, each instance fetches** + +```tsx +function UserList() { + const [users, setUsers] = useState([]) + useEffect(() => { + fetch('/api/users') + .then(r => r.json()) + .then(setUsers) + }, []) +} +``` + +**Correct: multiple instances share one request** + +```tsx +import useSWR from 'swr' + +function UserList() { + const { data: users } = useSWR('/api/users', fetcher) +} +``` + +**For immutable data:** + +```tsx +import { useImmutableSWR } from '@/lib/swr' + +function StaticContent() { + const { data } = useImmutableSWR('/api/config', fetcher) +} +``` + +**For mutations:** + +```tsx +import { useSWRMutation } from 'swr/mutation' + +function UpdateButton() { + const { trigger } = useSWRMutation('/api/user', updateUser) + return +} +``` + +Reference: [https://swr.vercel.app](https://swr.vercel.app) + +### 4.4 Version and Minimize localStorage Data + +**Impact: MEDIUM (prevents schema conflicts, reduces storage size)** + +Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data. + +**Incorrect:** + +```typescript +// No version, stores everything, no error handling +localStorage.setItem('userConfig', JSON.stringify(fullUserObject)) +const data = localStorage.getItem('userConfig') +``` + +**Correct:** + +```typescript +const VERSION = 'v2' + +function saveConfig(config: { theme: string; language: string }) { + try { + localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config)) + } catch { + // Throws in incognito/private browsing, quota exceeded, or disabled + } +} + +function loadConfig() { + try { + const data = localStorage.getItem(`userConfig:${VERSION}`) + return data ? JSON.parse(data) : null + } catch { + return null + } +} + +// Migration from v1 to v2 +function migrate() { + try { + const v1 = localStorage.getItem('userConfig:v1') + if (v1) { + const old = JSON.parse(v1) + saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang }) + localStorage.removeItem('userConfig:v1') + } + } catch {} +} +``` + +**Store minimal fields from server responses:** + +```typescript +// User object has 20+ fields, only store what UI needs +function cachePrefs(user: FullUser) { + try { + localStorage.setItem('prefs:v1', JSON.stringify({ + theme: user.preferences.theme, + notifications: user.preferences.notifications + })) + } catch {} +} +``` + +**Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled. + +**Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags. + +--- + +## 5. Re-render Optimization + +**Impact: MEDIUM** + +Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness. + +### 5.1 Defer State Reads to Usage Point + +**Impact: MEDIUM (avoids unnecessary subscriptions)** + +Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks. + +**Incorrect: subscribes to all searchParams changes** + +```tsx +function ShareButton({ chatId }: { chatId: string }) { + const searchParams = useSearchParams() + + const handleShare = () => { + const ref = searchParams.get('ref') + shareChat(chatId, { ref }) + } + + return +} +``` + +**Correct: reads on demand, no subscription** + +```tsx +function ShareButton({ chatId }: { chatId: string }) { + const handleShare = () => { + const params = new URLSearchParams(window.location.search) + const ref = params.get('ref') + shareChat(chatId, { ref }) + } + + return +} +``` + +### 5.2 Extract to Memoized Components + +**Impact: MEDIUM (enables early returns)** + +Extract expensive work into memoized components to enable early returns before computation. + +**Incorrect: computes avatar even when loading** + +```tsx +function Profile({ user, loading }: Props) { + const avatar = useMemo(() => { + const id = computeAvatarId(user) + return + }, [user]) + + if (loading) return + return
    {avatar}
    +} +``` + +**Correct: skips computation when loading** + +```tsx +const UserAvatar = memo(function UserAvatar({ user }: { user: User }) { + const id = useMemo(() => computeAvatarId(user), [user]) + return +}) + +function Profile({ user, loading }: Props) { + if (loading) return + return ( +
    + +
    + ) +} +``` + +**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders. + +### 5.3 Narrow Effect Dependencies + +**Impact: LOW (minimizes effect re-runs)** + +Specify primitive dependencies instead of objects to minimize effect re-runs. + +**Incorrect: re-runs on any user field change** + +```tsx +useEffect(() => { + console.log(user.id) +}, [user]) +``` + +**Correct: re-runs only when id changes** + +```tsx +useEffect(() => { + console.log(user.id) +}, [user.id]) +``` + +**For derived state, compute outside effect:** + +```tsx +// Incorrect: runs on width=767, 766, 765... +useEffect(() => { + if (width < 768) { + enableMobileMode() + } +}, [width]) + +// Correct: runs only on boolean transition +const isMobile = width < 768 +useEffect(() => { + if (isMobile) { + enableMobileMode() + } +}, [isMobile]) +``` + +### 5.4 Subscribe to Derived State + +**Impact: MEDIUM (reduces re-render frequency)** + +Subscribe to derived boolean state instead of continuous values to reduce re-render frequency. + +**Incorrect: re-renders on every pixel change** + +```tsx +function Sidebar() { + const width = useWindowWidth() // updates continuously + const isMobile = width < 768 + return