Back to the catalog

claude-todo-mirror

Mirrors Claude Code's TodoWrite state to per-session Markdown files with hierarchical checkboxes ([x] / [▶] / [ ]). Pin the file in VS Code,

Open source Open in the app JSON README (API)

About

Mirrors Claude Code's TodoWrite state to per-session Markdown files with hierarchical checkboxes ([x] / [▶] / [ ]). Pin the file in VS Code, Obsidian, or any Markdown viewer for an always-visible task view — no more scrolling back through chat to see what's left. Includes per-session isolation, a multi-session index, and slash commands (/todos-watch, /todos-pause, /todos-resume) for managing the mirror.

Details

Kind
Plugins
Topic
Developer tools
Publisher
bighaeil
Origin
marketplace
Category
ferramentas
Last push
2026-05-07T13:12:03Z
Repository state
ativo
Language
Python
License
MIT
Added
2026-08-30 01:48:58
Updated
2026-08-30 01:48:58
Origin id
bighaeil/claude-todo-mirror/claude-todo-mirror

README

# claude-todo-mirror

A Claude Code plugin that mirrors `TodoWrite` state to per-session markdown files
with hierarchical checkboxes — keep an always-visible task view open in VS Code,
Obsidian, or any markdown viewer instead of scrolling back through chat history.

## The problem

Claude Code's inline `TodoWrite` updates scroll out of view as the conversation
grows. When you want to know what's left, you scroll back through messages.
The built-in **Tasks** panel helps, but it's tied to one window and doesn't
expose a hierarchy or an external file you can pin in another editor.

## What this plugin does

On every `TodoWrite` call, a hook writes a markdown checklist for **the current
session** to `<project>/.claude/todos/session-<id>.md` and refreshes
`_index.md` with all sessions in this project. Open either file in VS Code,
Obsidian, or `tail -f` it from a terminal — it auto-updates.

### Sample output

```
# Session `abc12345-...`

**Project**: `/Users/me/code/my-app`
**Updated**: 2026-05-07 17:40:21 KST
**Progress**: 2/7 (29%)
**Now**: ▶ Verifying render_todos.py

---

- [x] Plugin scaffolding
- [x] Hook script
- [▶] Verifying render_todos.py
  - [ ] edge case: indented children
  - [ ] edge case: empty todos
- [ ] README + LICENSE
- [ ] GitHub push
```

### Hierarchy convention

`TodoWrite` items are flat by spec, so this plugin uses leading whitespace in
the `content` field as the hierarchy signal. Two spaces (or one tab) = one
indent level:

```python
TodoWrite([
    {"content": "Parent task",      "status": "in_progress", "activeForm": "Working"},
    {"content": "  Child task A",   "status": "pending",     "activeForm": "..."},
    {"content": "  Child task B",   "status": "pending",     "activeForm": "..."},
    {"content": "Sibling task",     "status": "pending",     "activeForm": "..."},
])
```

Tell Claude in your project's `CLAUDE.md` (or per-prompt) to follow that
convention when it writes nested todos.

### Status mapping

| TodoWrite status | Rendered |
| --- | --- |
| `pending` | `[ ]` |
| `in_progress` | `[▶]` |
| `completed` | `[x]` |

The first `in_progress` item is also pulled into a `Now: ▶ ...` header line so
you can see the active task at a glance.

### Per-session isolation

Each Claude Code session gets its own `session-<id>.md`. Run multiple sessions
in parallel — the plugin keeps them separate. `_index.md` summarizes all of
them in one table sorted by last activity:

```
| Session | Progress | Now | File |
| --- | --- | --- | --- |
| `abc12345` | 2/7 (29%) | Verifying render_todos.py | [session-abc...md](./...) |
| `def67890` | 4/4 (100%) | -                         | [session-def...md](./...) |
```

## Install

This repo is a **single-plugin marketplace** — `marketplace.json` is committed
at `.claude-plugin/marketplace.json` so you can install it via the standard
plugin commands.

> **Heads up**: `/plugin ...` commands only work in the Claude Code **CLI**
> (terminal), not in the Desktop app. Once installed, slash commands like
> `/todos-watch` work in both Desktop and CLI.

Open a Claude Code CLI session and enter these two commands **separately**
(do not paste them on the same line — Claude Code parses the second one as
part of the first command's URL):

```
/plugin marketplace add bighaeil/claude-todo-mirror
```

Then, on its own:

```
/plugin install claude-todo-mirror@claude-todo-mirror
```

When the install dialog asks for scope, **"Install for you (user scope)"** is
the right choice for personal use — the plugin becomes available across every
project and any Claude Code surface (CLI, Desktop).

For local development without going through the marketplace:

```
claude --plugin-dir /path/to/claude-todo-mirror
```

After install, every `TodoWrite` call in any project will create
`.claude/todos/` under that project's root.

### Manual hook registration (without `/plugin install`)

If you can't or don't want to use the plugin marketplace, drop this into your
project's `.claude/settings.local.json` (or user `~/.claude/settings.json`):

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "TodoWrite",
        "hooks": [
          {
            "type": "command",
            "command": "python3 /absolute/path/to/claude-todo-mirror/scripts/render_todos.py"
          }
        ]
      }
    ]
  }
}
```

### Recommended workflow

1. Open `<project>/.claude/todos/_index.md` in VS Code (or Obsidian) and pin
   the tab.
2. Or open the active session file `session-<your-session>.md` directly.
3. Work as usual — the file refreshes on every `TodoWrite` call.

For multi-session overview, the `_index.md` is the single source of truth.

## Live terminal monitor (`/todos-watch`)

> **Prerequisite** — this command depends on the macOS `watch` CLI. Without
> it, the new Terminal window opens but immediately exits with
> `command not found: watch`.
>
> ```bash
> brew install watch
> ```
>
> The core hook (`TodoWrite` → markdown mirroring) only needs Python 3 and
> works on any OS. `watch` is a dependency **for the terminal live view
> only** — if you open the markdown files directly in VS Code or Obsidian,
> you don't need it.

Don't want to leave an editor pinned? Run the bundled slash command from any
Claude Code session:

```
/todos-watch
```

This opens a new **macOS Terminal** window running `watch -d` against this
project's `.claude/todos/`. It refreshes every 10 seconds and highlights any
line that changes — every `TodoWrite` call shows up live without you scrolling
back through chat or copy-pasting paths.

### Environment compatibility

| Environment | `/todos-watch` | Fallback |
| --- | --- | --- |
| macOS + `watch` installed | Works | — |
| macOS + `watch` missing | Fails | `brew install watch` |
| Linux | Unsupported (osascript-based) | Run `bash <plugin-dir>/scripts/watch-todos.sh "$PWD"` directly |
| Windows | Unsupported | Same as Linux, via WSL or git-bash |

The `<plugin-dir>` path under `~/.claude/plugins/cache/claude-todo-mirror/` is
the script's location once installed via the marketplace.

## Pause and resume mirroring (`/todos-pause`, `/todos-resume`)

Want to keep the plugin installed but temporarily stop markdown mirroring
(e.g. during a quick scratchpad session you don't want to record)? Two
slash commands toggle a per-project flag:

```
/todos-pause     # creates .claude/todos/.paused → mirroring suspended
/todos-resume    # removes the flag → mirroring active again
```

When `.paused` exists, every `TodoWrite` call still triggers the hook, but
`render_todos.py` short-circuits on the flag and writes nothing — no
`session-*.md` updates, and any open `/todos-watch` monitor stays frozen.

Notes:

- The flag is **per-project** (one flag per `${CLAUDE_PROJECT_DIR}/.claude/todos/`).
  Pausing one project does not affect mirroring in another.
- `TodoWrite` token usage is **not affected** — Claude still calls the tool
  based on its own judgment. Only the file mirroring is suppressed.
- The `.paused` file itself is empty; you can also create or remove it
  manually with `touch` / `rm` if you prefer.

## Requirements

- Claude Code (any version that supports the plugin system + `PostToolUse` hooks)
- Python 3 (any 3.8+ available on `python3` in `PATH`)
- macOS + `watch` (only for `/todos-watch`; the core hook works on any platform)

No other dependencies — the hook is a single self-contained Python file.

## How it works

```
hooks/hooks.json
  └─ PostToolUse(matcher=TodoWrite)
       └─ scripts/render_todos.py    (reads stdin JSON, writes markdown)
            └─ short-circuits early if .claude/todos/.paused exists

commands/todos-watch.md
  └─ /todos-watch
       └─ scripts/launch-watch.sh    (osascript → new Terminal window)
            └─ scripts/watch-todos.sh   (watch -d on the mirror files)

commands/todos-pause.md
  └─ /todos-pause
       └─ scripts/pause.sh           (touch .claude/todos/.paused)

commands/todos-resume.md
  └─ /todos-resume
       └─ scripts/resume.sh          (rm -f .claude/todos/.paused)
```

The hook runs synchronously after every `TodoWrite`, parses the
`tool_input.todos` array, and rewrites the session file + index. Failures are
logged to `stderr` and never block the tool — at worst your file goes stale.

## Configuration

None. The plugin uses three well-known paths under `${CLAUDE_PROJECT_DIR}/.claude/todos/`:

- `session-<session_id>.md` — per-session checklist (auto-generated by the hook)
- `_index.md` — summary table of all sessions in the project (auto-regenerated)
- `.paused` — empty toggle flag; when present, the hook writes nothing.
  Created by `/todos-pause`, removed by `/todos-resume`. You can also create
  or remove it manually with `touch` / `rm`.

Add `.claude/todos/` to your project's `.gitignore` if you don't want
session files committed.

## License

MIT — see [LICENSE](./LICENSE).

---

## 한국어 요약

Claude Code의 `TodoWrite` 결과는 인라인 마크다운으로만 출력되고 대화가 길어지면
스크롤 위로 사라집니다. 진행 상황을 보려면 매번 위로 거슬러 올라가야 하죠.

이 플러그인은 `TodoWrite` 호출이 일어날 때마다 **현재 세션의 todo를
계층 체크박스가 있는 markdown 파일로 자동 저장**합니다.

```
<프로젝트>/.claude/todos/
├── session-<세션ID>.md    # 채널별 체크리스트
└── _index.md              # 모든 채널 요약 표
```

VS Code · Obsidian · 마크다운 뷰어 등에 파일을 한 번 열어 두기만 하면, 매번
자동으로 갱신되는 살아있는 todo 뷰가 됩니다. 채팅 채널이 여러 개여도 세션
ID로 분리되어 헷갈리지 않습니다.

### 들여쓰기 규칙

```python
TodoWrite([
    {"content": "상위 작업"},
    {"content": "  하위 작업 1"},   # 2 space → 1 단계 들여쓰기
    {"content": "  하위 작업 2"},
])
```

`CLAUDE.md`에 이 규칙을 알려주면 Claude가 자동으로 계층 todo를 만들어 줍니다.

### 설치

> **주의**: `/plugin ...` 명령은 **Claude Code 터미널 CLI에서만** 동작합니다
> (Desktop 앱에서는 안 됨). 설치 후 `/todos-watch` 같은 슬래시 명령은 Desktop과
> CLI 양쪽에서 모두 사용 가능합니다.

CLI 세션에서 두 명령을 **각각 따로** 입력하세요 — 한 줄에 같이 붙이면 Claude
Code가 두 번째 명령을 첫 명령의 URL 일부로 해석해서 실패합니다.

```
/plugin marketplace add bighaeil/claude-todo-mirror
```

그 다음 별도로:

```
/plugin install claude-todo-mirror@claude-todo-mirror
```

설치 스코프 선택 다이얼로그가 뜨면 **"Install for you (user scope)"** 권장
— 모든 프로젝트에서 사용 가능하고 Desktop에서도 슬래시 명령이 잡힙니다.

또는 hook 직접 등록 (위 영어 섹션 "Manual hook registration" 참조).

### 터미널 실시간 모니터링 — `/todos-watch`

`/todos-watch`로 새 macOS Terminal 창에서 todo 변경을 실시간 확인할 수 있습니다.
사전에 `brew install watch` 필요 (macOS 기본 미설치).

상세 사전 요구사항·OS 호환성·Linux/Windows 대안은 위 영문 섹션
[Live terminal monitor (`/todos-watch`)](#live-terminal-monitor-todos-watch)
를 참조하세요.

### 미러링 일시정지·재개 — `/todos-pause`, `/todos-resume`

플러그인은 그대로 두고 markdown 미러링만 잠시 끄고 싶을 때 사용합니다.

- `/todos-pause` → 현재 프로젝트의 `.claude/todos/.paused` flag 생성. 이후
  `TodoWrite` 호출이 일어나도 markdown 파일은 갱신되지 않습니다 (열어둔
  `/todos-watch` 모니터도 정지된 상태로 보임).
- `/todos-resume` → flag 제거. 다음 `TodoWrite` 호출부터 다시 갱신됩니다.

flag는 **프로젝트별**이라 다른 프로젝트의 미러링에는 영향 없습니다. 또한
`TodoWrite`의 토큰 사용 자체는 그대로 — Claude가 도구를 호출하는 것은 막지
않고 markdown 저장만 중단합니다.

상세는 위 영문 섹션 [Pause and resume mirroring](#pause-and-resume-mirroring-todos-pause-todos-resume) 참조.

More