---
title: "Markdown table of contents: write one by hand or generate it"
slug: markdown-table-of-contents
excerpt: "Add a linked table of contents to a long .md file, by hand or with pandoc, and keep it right after an agent renames headings."
quickAnswer: "Markdown has no table of contents syntax in the CommonMark spec. A table of contents is just a list of links, where each link points at a heading, like [Rollback steps](#rollback-steps). Nest the list to match heading levels. You can type it by hand or have a tool build it. Pandoc builds one with --toc, but only when you also pass -s (--standalone), and --toc-depth sets how many heading levels it lists (3 by default). Markers like [TOC] are extensions: some tools swap them for a contents list, others print them as text. When an agent renames a heading, its link in the list can go stale, so re-check the list after each edit."
keyTakeaways:
  - "CommonMark has no table of contents element. You build one from a plain list of links whose targets start with #."
  - "Indent a sub-entry under its parent so it becomes a nested list. With a - marker, two spaces is enough."
  - "Markers like [TOC] or [[_TOC_]] only work in tools that support them. Other tools may show `[TOC]` as plain text."
  - "Pandoc's --toc has no effect unless you also pass -s or --standalone. --toc-depth defaults to 3 levels."
  - "Sashmark is in development and has not been released. Its v1 plan has no table of contents or outline feature."
publishedDate: 2026-10-20
dateModified: 2026-10-20
author: Bobby Huang
cluster: B
role: post
status: published
faq:
  - q: Does Markdown have a built-in table of contents tag?
    a: "Not in CommonMark. The spec has headings, lists and links, but no contents element. Markers such as [TOC] come from extensions, so they only work in tools that know them."
  - q: Why does my table of contents link jump to the page top or do nothing?
    a: "The target after the # has to match the id your viewer gave the heading. Headings get no ids from the spec, so a viewer that makes ids uses its own rules. If an agent reworded the heading, the id likely changed too. Fix the target in the list to match."
  - q: How deep should a table of contents go?
    a: "For most plan files, level 2 headings are enough, with level 3 only under long sections. Pandoc lists three levels unless you set --toc-depth to another number."
related:
  - markdown-headings-anchor-links
  - large-markdown-files
  - read-a-claude-code-plan-file-without-an-ide
---

A long agent plan can run to dozens of sections. Scrolling to find Risks or Open questions gets old fast. A short linked list at the top fixes that: one click per section.

Markdown gives you the parts to build that list, but not a ready-made tag. You can type it yourself in a few minutes, or let a converter like pandoc write it. Either way, the hard part comes later, when an agent edits the headings it points at.

## There is no TOC tag in CommonMark

The CommonMark spec defines headings, lists and links. It defines no table of contents. A TOC is a list where each item is a link, and each link's target is `#` plus a heading id.

That means everything in a TOC follows the normal rules. A list item starts with a marker such as `-`, `*` or `+`. A link is text in square brackets followed by a target in parentheses. Nothing about it is special.

The catch sits in the target. A link may point at `#fragment` under the spec, yet the spec assigns no id to any heading. If your viewer makes ids, it follows its own rules, and some viewers make none. The post on [how heading ids work](markdown-headings-anchor-links.md) covers how ids get made and why links to them break, so read that first if your links misfire.

## Write a TOC by hand

A hand-made TOC is a nested list. Top-level items point at level 2 headings. Indented items point at level 3 headings under them.

```markdown
## Contents

- [Summary](#summary)
- [Rollout](#rollout)
  - [Install steps](#install-steps)
  - [Rollback steps](#rollback-steps)
- [Open questions](#open-questions)
```

With a `-` marker and one space, a sub-item needs two spaces of indent to nest. If you indent too little, it lands at the top level instead.

Build it like this:

1. **Get the heading list.** Copy every level 2 and level 3 heading, in order.
2. **Write one link per heading.** The link text can match the heading word for word, or be shorter.
3. **Set each target.** Use the id your viewer makes for that heading. Check one or two by clicking before you write the rest.
4. **Indent the sub-items.** Two spaces under a `-` item is enough.
5. **Click every link once.** A link that does nothing most likely has a target that doesn't match.

Keep the TOC near the top, under a short intro. Some people add a "Back to contents" link after each long section. That's an ordinary link too, pointing at the id of the Contents heading.

## [TOC] and other markers

You may see a line like `[TOC]` or `[[_TOC_]]` in someone's file. These come from extensions. A tool that supports one may swap the marker for a contents list when it renders the page.

Plain CommonMark reads it differently. A bracketed word only becomes a link when the file also has a matching link reference definition, so `[TOC]` on its own stays as text. Many viewers will print `[TOC]` exactly as typed. In `[[_TOC_]]`, the underscores may turn TOC into italics instead. If you share a file with people who use different tools, a hand-built list is the safe bet.

## Generate one with pandoc

Pandoc is a command-line tool that converts between markup formats. It can write the TOC for you with `--toc` (long form `--table-of-contents`).

```sh
pandoc -s --toc --toc-depth=2 plan.md -o plan.html
```

Three points from the pandoc manual matter here:

- **`-s` is required.** The manual says `--toc` "has no effect unless -s/--standalone is used." It also has no effect on man, docbook4, docbook5 or jats output.
- **Depth defaults to 3.** Without `--toc-depth`, pandoc lists level 1, 2 and 3 headings.
- **Ids are turned on for you.** TOC entries need heading ids to link to, so `--toc` forces pandoc's `auto_identifiers` extension on.

Running it gives you a new file with the TOC built in. Your source `.md` stays as it was. Markdown is not on the manual's list of formats where `--toc` does nothing, so you can try `-o plan-toc.md` and paste the list it writes into your file. Check the output before you trust it, because pandoc's ids may not match the ones your viewer makes.

## List the headings from the shell

To build or check a TOC by hand, you need the headings and their line numbers. One line gets them on Mac or Linux:

```sh
grep -nE '^#{1,6} ' plan.md
```

```text
1:# Release plan
5:## Summary
12:## Rollout
14:### Install steps
31:### Rollback steps
48:## Open questions
```

Each hit shows where the heading sits and what it says. That's your TOC, minus the link syntax.

Two caveats. First, this pattern also matches `#` lines inside fenced code blocks. The spec treats fence content as literal text, so a shell comment like `# install deps` in a code sample is not a heading, but grep will still list it. Skip any hit that sits between fence lines. Second, the pattern misses a few real headings: one with a tab after the `#` signs, one with up to three leading spaces, or a heading underlined with `===` or `---` on the next line. If a heading you expect is missing, look for those.

For a file big enough that the list itself gets long, the guide to [very long Markdown files](large-markdown-files.md) has more ways to size it up and split it.

## Keep the TOC in sync after an agent edits

A TOC is a copy of your headings. When an agent rewords one, the copy goes stale. The link text still shows the old name, and the target may name an id the page no longer has.

Run this check after any agent pass on a file with a TOC:

1. **Save the heading list before the edit.** `grep -E '^#{1,6} ' plan.md > headings-before.txt`
2. **Save it again after.** Same command, into `headings-after.txt`. Leave out `-n` here, so line numbers don't make every later heading look changed.
3. **Compare the two.** `diff -u headings-before.txt headings-after.txt` shows added, removed and renamed headings.
4. **Fix each changed entry.** Update the link text and the target in the TOC for every heading that moved or changed.
5. **Search for other links to the old id.** `grep -n '](#rollback-steps)' plan.md` finds links further down the file that point at it.
6. **Click through the TOC.** One pass in your viewer confirms each link lands.

You can hand part of this to the agent. A line in its project notes works:

> When you rename, add or remove a heading in a file that has a Contents list, update the Contents list in the same edit. Keep link text and targets matched to the new heading.

If you review agent plan files, see [where plan files get saved and how to open one](read-a-claude-code-plan-file-without-an-ide.md).

## When a TOC isn't worth it

Skip it for short files. If the whole file fits on a screen or two, or it has three or four sections, a TOC adds lines without saving clicks. It is also one more thing to keep in sync.

A TOC pays off when a document runs long and has many level 2 sections. It works best when the file is read more often than it is rewritten. A file an agent rewrites every few minutes may change faster than you can keep its TOC right. For those, a heading search in your viewer may serve you better. The [basic syntax reference](markdown-syntax-guide.md) has the ground rules behind all of this.

## Where Sashmark fits

A desktop tool called Sashmark is being made so you can read and edit Markdown, aimed at people who review files their agents keep changing. It is in development and has not been released. For v1, rendered headings, lists and links are planned, so a contents list you build by hand would show as a list of links. No table of contents or outline feature is planned. The app itself has no AI features. Our [planned features](/features), [help pages](/help) and [roadmap](/roadmap) spell out what is planned so far.

To follow along as the app is built, go to [sashmark.com](https://www.sashmark.com/).
