Claude Code: The Perfect Development Loop
I've written two slash commands for Claude Code that form the core of my development workflow: /todo implements one task and /commit commits it.
This post is part of my Claude Code setup series.
Understanding Slash Commands
Claude Code comes with built-in slash commands like /pr-comments, which are essentially shortcuts for specific prompts. You can also create custom slash commands.
- Global commands: Place them in
$HOME/.claude/commands/example.md - Project commands: Place them in
./.claude/commands/example.md
Both are invoked with /example. If a command exists in both locations, the project-specific version takes precedence.
You can find more examples and command definitions in the claude-code-plugins repository.
The Development Loop
When building larger features, I work through a task list in .llm/todo.md:
- Run
/todoto implement the next task - Test and debug manually
- Run
/committo commit the changes - Run
/compactto clean up the conversation history - Repeat
Creating the Task List
When planning larger changes, I tend to chat with a thinking model in the browser. At the end, I ask for a task list:
Write a task list that we can work through to implement this idea, as a markdown checklist.
Each checkbox should represent a task that we can implement and commit on its own.
Arrange the tasks in the order we need to implement them, not in order of importance.
Example format:
- Add a test case for feature ABC.
- It's ok for the test to be failing at this point.
- Implement feature ABC.
- Delete DEF
- Replace usages of DEF with ABC.
I save this list to .llm/todo.md. I've configured Claude to ignore the .llm directory in version control.
/todo
This command tells Claude to implement one task from the list.
---
name: tasks
description: Work with markdown-based task lists in .llm/todo.md files. Use when managing tasks, working with todo lists, extracting incomplete tasks, marking tasks complete, or implementing tasks from a task list.
---
# Markdown Task Management
The task list lives in `.llm/todo.md`. Inspect and change it only through the scripts below: the file grows large, and the scripts return just the task you need instead of loading the whole list into context.
`<plugin-root>` is `${CLAUDE_PLUGIN_ROOT}` in Claude Code. In Codex, it is the plugin root that contains this `skills/tasks/SKILL.md` file.
Every script exits 0 on success and 1 on error.
## Scripts
### task_get.py - Extract Next Task
```bash
python <plugin-root>/scripts/task_get.py .llm/todo.md
```
Prints the first `[ ]` task with all indented context lines below it. Exits 1 when the file does not exist.
To see more of the queue, select incomplete tasks by 1-based position. Positions count only `[ ]` tasks; `[x]`, `[!]`, and `[>]` tasks are skipped.
```bash
python <plugin-root>/scripts/task_get.py .llm/todo.md --index 3
python <plugin-root>/scripts/task_get.py .llm/todo.md --range 1-10
python <plugin-root>/scripts/task_get.py .llm/todo.md --limit 10
```
Each selected task prints as its checkbox line and context, with a blank line between tasks. `--index` and a `--range` that starts past the last incomplete task exit 1; a `--range` or `--limit` that runs past the end prints the tasks that exist.
### task_add.py - Add New Task
```bash
python <plugin-root>/scripts/task_add.py .llm/todo.md "Task description
Context line 1
Context line 2"
```
Appends a `[ ]` task, creating `.llm/todo.md` if needed and preserving indentation in multi-line strings.
Add a batch of tasks in one shell command, chaining one call per task with `&&`, so concurrent sessions writing the same file are unlikely to interleave:
```bash
python <plugin-root>/scripts/task_add.py .llm/todo.md "First task
Context line 1" && \
python <plugin-root>/scripts/task_add.py .llm/todo.md "Second task"
```
### task_mark.py - Mark Task
```bash
python <plugin-root>/scripts/task_mark.py .llm/todo.md
python <plugin-root>/scripts/task_mark.py .llm/todo.md --marker='!' --reason='precommit failed on the parser rewrite'
```
Changes the first `[ ]` to `[x]`, or to any single non-space character passed as `--marker`. Exits 1 when no incomplete task remains.
`--reason` appends an indented line to the end of the task body:
```markdown
- [!] Require authentication on API routes.
Reuse `validateJwt` from `/workspace/project/src/auth/tokens.ts`.
Blocked 2026-08-19 session 94aec27b: validateJwt does not exist; the repo uses `verifyToken`
```
The reason travels with the task through archive and recovery, and `task_get.py` hands it to the next attempt. Quote the concrete failure (the failing command, the assertion, the missing symbol) rather than restating the task.
The line's label follows the marker: `Blocked` for `!`, `Completed` for `x`, `Claimed` for `>`, and `Marked [<marker>]` for any other.
`--reason` is required with `--marker='!'`; without it the script exits 1 and leaves the file untouched.
To mark a task other than the first, name it:
```bash
python <plugin-root>/scripts/task_mark.py .llm/todo.md --index 3 --marker='>'
python <plugin-root>/scripts/task_mark.py .llm/todo.md --title 'Require authentication on API routes.'
```
`--index` counts `[ ]` tasks the same way `task_get.py` does. `--title` matches the first `[ ]` or `[>]` task whose text after the checkbox equals the title exactly, so it can complete or block a task that was claimed with `[>]`. Positions shift once a task is marked, so prefer `--title` when marking several tasks in turn. A missing title or out-of-range index exits 1 and leaves the file untouched.
### task_archive.py - Archive Task List
```bash
python <plugin-root>/scripts/task_archive.py .llm/todo.md
```
Moves the file to `.llm/YYYY-MM-DD-todo.md`. Blocked `[!]` tasks are carried forward into a fresh `.llm/todo.md`, still marked `[!]`. Prints the archive path and the carried-forward count.
### task_unblock.py - Recover Blocked Tasks
```bash
python <plugin-root>/scripts/task_unblock.py .llm --dry-run
python <plugin-root>/scripts/task_unblock.py .llm
```
Scans every `.llm/*todo*.md`, including the live `.llm/todo.md`, and moves each `[!]` task into `.llm/todo.md` as an open `[ ]` task stamped with an indented `Recovered <yyyy-mm-dd> session <session-id>` line. Existing `Blocked` lines are kept. Recovery is a move, so re-running cannot duplicate a task; emptied archive files stay on disk. Exits 0 when nothing is blocked.
Recovery rewrites historical archives. Always run `--dry-run` first and confirm the report with the user.
## Task Format
A task starts with one of these markers:
- `[ ]` - Ready
- `[x]` - Completed
- `[!]` - Blocked after a failed attempt
Each task is extracted and executed in isolation, so it must carry all the context needed to implement it in indented lines:
- Absolute file paths
- Exact function/class names
- Existing patterns to follow
- Dependencies and prerequisites
- Expected outcome
Repeat shared context in every related task. Never reference other tasks.
## Plan Files
Before adding tasks from a plan, store the plan at `.llm/plans/<yyyy-mm-dd>-<descriptive-name>.md` (for example, `2025-12-04-thread-safety-tests.md`), creating the directory if needed. A Claude Code plan-mode file under `~/.claude/plans/` is moved there, not copied. Include the archived plan's absolute path in every task.
"Think hard" is a magic word that triggers extended reasoning.
/commit
After implementing and testing a task, I use /commit to create a commit:
---
name: commit
description: Commit local changes with careful staging and single-line messages. Use for all git commit operations.
---
# Commit
Use the `code:cli` and `git:git-workflow` skills.
## Inspect Context
```bash
git status
git diff --cached
git diff
git branch --show-current
git log --oneline -10
```
## Stage Files
Stage only the files modified for the current task, individually:
```bash
git add <file1> <file2>
```
Never use `git add .`, `git add -A`, `git commit -am`, or anything else that stages unrelated changes.
## Commit Message
If the user pasted a compiler or linter error, or the task is explicitly a fixup for a known commit, use `git commit --fixup <sha>`.
Otherwise write one line following the format in `git:git-workflow`. The prompt is the intent, not the message text: borrow its language, but distill a long or multi-line prompt to one line.
Echo exactly:
```text
Running: `git commit --message "<message>"`
```
Then commit without confirming again with the user:
```bash
git commit --message "<message>"
```
If pre-commit hooks modify files, stage those files individually and retry. Never use `git commit --no-verify`.
A commit that a hook stopped was never made, so fix the problem, stage the files, and run `git commit` again. Never run `git commit --amend`: after a failed commit, `--amend` rewrites the previous commit, which usually belongs to an earlier task. To change a commit that already exists, make a new commit or a `git commit --fixup <sha>` commit. A stray fixup that never folds in is easy to clean up later with `git history reword`, and a wrongly amended commit is not.
You don't need a custom command to commit - and Claude will offer to commit without being asked. But this gives me control over timing while leveraging Claude's ability to write good summaries.
Managing the Context Window with /compact
The /compact command is built in but not well documented. It's built on a markdown prompt to summarize the conversation, then replaces Claude's context with that summary.
After completing a task, I need to make room in the context window for the next task. I used to run /compact if the next task was related, and /clear if unrelated. Now I just always run /compact.
This is also a good time to restart with claude --continue if it has been running too long or if there's a new version available.
Summary
This workflow breaks large features into manageable steps.
These commands work best when combined with proper Claude Code configuration.
For more custom commands, see Claude Code Utility Commands.
