Skip to main content

Claude Code: The Perfect Development Loop

· 4 min read
Craig P. Motlin
Software Engineer

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:

  1. Run /todo to implement the next task
  2. Test and debug manually
  3. Run /commit to commit the changes
  4. Run /compact to clean up the conversation history
  5. 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.

.claude-prompts/commands/todo.md
---
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.

View on GitHub

"Think hard" is a magic word that triggers extended reasoning.

/commit​

After implementing and testing a task, I use /commit to create a commit:

.claude-prompts/commands/commit.md
---
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.

View on GitHub

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.