Repair malformed markdown code fence closings...
Scan and repair malformed closing fences in markdown files. Closing fences must never contain language identifiers.
| Trigger Phrase | Operation |
|---|---|
fix markdown fences |
Scan and repair malformed fence closings |
repair code block closings |
Fix closing fences with language identifiers |
markdown rendering broken |
Diagnose and fix fence issues |
code blocks bleeding into content |
Fix unclosed or malformed fences |
validate markdown code blocks |
Check all fences for correctness |
| Symptom | Cause | Fix |
|---|---|---|
| Code block bleeds into text | Closing fence has language identifier | Insert a bare closing fence above it; the line then opens the next block |
| Nested blocks render wrong | Missing closing fence before new opening | Insert closing fence |
| Content cut off at end of file | Unclosed code block | Append closing fence |
Use this skill when:
```python instead of ```)Use manual editing instead when:
Do not walk the file by hand. Fence tracking is a state machine, and
fix_fences.py runs it.
FILE:LINE: KIND: TEXT and exits 1 when it finds any.python3 "${COPILOT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-.claude}}/skills/fix-markdown-fences/scripts/fix_fences.py" FILE_OR_DIR
Read the report. Two kinds appear:
malformed_closing: a closing fence carries a language identifier, so
the block bleeds into the following prose.unclosed_block: the file ends with a block still open.Decide, then write. Repair is best-effort on an ambiguous file. When a defect cluster sits inside documentation that shows fenced markdown, the author usually wanted a wider container fence (four backticks around a three-backtick example), not the closing fence the repair inserts. Widen the container by hand in that case. Otherwise:
python3 "${COPILOT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-.claude}}/skills/fix-markdown-fences/scripts/fix_fences.py" FILE_OR_DIR --write
git diff and
confirm only fence lines moved.Detects and repairs malformed fence closings. Reporting is the default;
--write is required to modify a file.
python3 "${COPILOT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-.claude}}/skills/fix-markdown-fences/scripts/fix_fences.py" PATH [PATH ...]
python3 "${COPILOT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-.claude}}/skills/fix-markdown-fences/scripts/fix_fences.py" PATH --write
python3 "${COPILOT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-.claude}}/skills/fix-markdown-fences/scripts/fix_fences.py" PATH --json
Options: --write repairs in place, --json emits machine-readable output,
--pattern sets the glob for directory scans (default *.md). Paths default
to the current directory. .git, node_modules, .venv, venv, and __pycache__
are skipped.
Fence matching follows CommonMark, which is what keeps the tool from damaging documentation:
* * * matches the bullet grammar, and a list may interrupt a paragraph only
when the item is non-empty and, if ordered, starts at 1 (leading zeros do not
change that start). That last veto is scoped to the item the paragraph lives
in: a marker indented below the content column closes the item, the paragraph
closes with it, and the marker is then judged at the outer level where no
paragraph is open. A link reference definition ([foo]: /url) is its own
block and leaves no paragraph open, so a list may start after one; but a
definition cannot interrupt a paragraph that is already open, its
destination and title must be complete ([foo]: <broken is prose), its
destination balances parentheses at any depth and may escape either
angle delimiter while a title may escape its own and may run across
lines until that delimiter arrives, a continuation belongs to the same
leaf block and so keeps its meaning at any indent, and
either the destination or a bare title may sit on the following line, and
a blank line, a fenced block, a list marker or an indented code block
cancels a definition still waiting for one, while a line that does
continue one is a lazy continuation and does not close the item holding
it. A label of only whitespace is not a definition at all. A blank line
directly after an empty marker closes the item, and a paragraph
continuation line may dedent without closing it.
Getting any of these wrong moves the content column, which moves what counts
as a fence.- - a opens two items. A block also ends when the item holding
it ends, with no closing marker, so a line that dedents below it closes it.
Without either rule the tool kept a block open past its real end, and
--write appended a closing fence to documents already well formed.--write appends a closer anyway. Measured across
all seven CommonMark HTML block types, opener terminated and unterminated,
20 of 20 shapes are written to. Do not run --write unattended over
documents containing raw HTML blocks. A blockquote prefix is never stripped,
and that costs two different things. A fence inside > is invisible, which is a miss: six
shapes diverge and --write changes none. A blockquote INTERRUPTING a
paragraph is worse: CommonMark ends the paragraph and lazily continues the
quote, so a following 2. opens a list, while we keep the paragraph open
and --write appends a closer to a balanced document. Two of twelve
measured shapes do that. A backslash in a link destination escapes whatever
follows it, where CommonMark escapes only ASCII punctuation, so an escaped
space and an escaped tab are read wrongly: 62 of 64 shapes agree. The spec
rule was measured before being believed and is worse, 34 of 64 with 30
--write corruptions, so the permissive rule stays. The escaped-TAB half is
destructive on its own account, because tabs are expanded to four-column
stops before the grammar sees them, so what the permissive rule eats depends
on the label's length: nine of eleven single-line label lengths turn a
balanced document into an unclosed one, and the two that do not are the two
that land on a tab stop. Escaped space and the next-line destination path
corrupt none of eleven. And a setext ===
underline
directly under a list item, followed by a lazy continuation and then an
indented fence, leaves that fence unseen. This one is DESTRUCTIVE too, and
it was recorded here as a miss on a measurement that did not hold: over nine
shapes, seven are rewritten and six go in balanced and come out unclosed.
The smallest is five lines, - item / === / lazy / two-space fence /
out, where the list item ending closes the fence for CommonMark and
--write appends a second one at top level. --- under
the same item already agrees. The scanners' agreement with a CommonMark
reference is measured by the fuzz baselines in the repository's test suite.Files are read and written as bytes, so CRLF and CR endings survive, a UTF-8
BOM survives, and every separator str.splitlines would swallow (U+000B,
U+000C, U+001C, U+001D, U+001E, U+0085, U+2028, U+2029) stays put. Each line keeps its own terminator, so
a mixed-ending file is not normalized. Repair is idempotent.
Exit codes (ADR-035):
0 no defects found, or --write repaired every defect it found1 report mode found at least one defect; nothing was written2 a requested path does not exist, or a file could not be read or writtenKept so a reader can audit the script, not so the agent can run it by hand.
Track fence state while scanning line by line:
python3 "${COPILOT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-.claude}}/skills/fix-markdown-fences/scripts/fix_fences.py" PATH
echo "exit=$?" # 0 = clean, 1 = defects found, 2 = bad input
git diff shows only fence lines added, no content modifications.--write.| Avoid | Why | Instead |
|---|---|---|
| Manually searching for bad fences | Error-prone in large files, and the agent cannot track fence length by eye | Run fix_fences.py |
| Simulating the state machine in-context | The script already does it, exactly and for free | Read the script's report |
Running --write across a whole repo unreviewed |
A repair inside nested documentation is often the wrong fix | Report first, review, then write per path |
| Copying opening fence line to close a block | Creates the exact bug this skill fixes | Close with the opener's character, no info string, at least the opener's length |
| Fixing fences without tracking block state | Misidentifies nested vs sequential blocks | Run fix_fences.py, which tracks it |
When generating markdown with code blocks:
The shipped script does this for you; fix_fences.py is the
implementation, and the Reference section above is the algorithm it runs. An
earlier revision of this file inlined a copy of that parser here under
"Implementation: Python (Recommended)". It was the pre-CommonMark version,
which had no fence-length rule and so corrupted any document showing a
three-backtick example inside a four-backtick container. It is gone rather
than fixed: a second copy of a parser drifts from the one that ships.
# Find files with potential issues
# Single quotes throughout: a backtick inside DOUBLE quotes opens command
# substitution, and this line used to fail `bash -n` for that reason.
grep -rEn --include="*.md" -- '```\w+' . | grep -vE '^[^:]*:[0-9]*:[[:space:]]*```\w+[[:space:]]*$'
An earlier revision also inlined a PowerShell rewriter here. It is gone for
the same reason, and measurement is why rather than symmetry: run on a CRLF
file it rewrote every ending to LF, which the Edge Cases list below
explicitly promises it does not do, and a ~~~python block left unclosed
was invisible to it because it matched backticks only. The shipped script
closes that tilde block and preserves the CRLF. A copy that drifts this far
while sitting under the same heading is worse than no example.
\n, \r\n and \r are preserved per line, as is a UTF-8 BOM and the presence or absence of a trailing newline