LLMs generate ASCII diagrams with broken borders. This fixes them.
Before After
┌─────────────────────┐ ┌─────────────────────┐
│ Component A │ → │ Component A │
│ with content │ │ with content │
└─────────────────────┘ └─────────────────────┘
LLMs generate ASCII diagrams with misaligned right borders. The top and bottom boundary lines (┌───┐, └───┘) are usually correct because they're repetitive patterns. But content lines with variable text end up short:
┌─────────────────────────┐
│ This line is too short│ ← Right border doesn't align
│ API Gateway │ ← Same problem here
└─────────────────────────┘
This happens because:
- LLMs count characters inconsistently when content varies
- Boundary lines are easy (repeat
─until done) - Content lines require precise space calculation
See the examples/ directory for more before/after examples.
npm install -g boxfixOr use directly with npx:
npx boxfix input.md# Output to stdout
boxfix input.md
# Fix files in place
boxfix input.md --in-place
boxfix **/*.md -i
# Check mode for CI (exit code 1 if fixes needed)
boxfix --check *.md
# JSON output for agents/tooling
boxfix input.md --json
# Preview changes without modifying
boxfix input.md --dry-run
# Multiple files
boxfix doc1.md doc2.md --in-place| Flag | Short | Description |
|---|---|---|
--in-place |
-i |
Modify files in place |
--output <file> |
-o |
Write to specific file (single input only) |
--check |
-c |
Check mode - exit 1 if fixes needed |
--json |
-j |
Output results as JSON |
--dry-run |
-d |
Preview changes without modifying |
--quiet |
-q |
Suppress output except errors |
--hook |
Read JSON from stdin, extract file path, fix in-place (for AI agents) |
{
"files": [
{
"file": "input.md",
"linesFixed": 3,
"blocksProcessed": 2,
"diagramsFound": 1
}
],
"summary": {
"totalFiles": 1,
"filesWithFixes": 1,
"totalLinesFixed": 3,
"totalDiagramsFound": 1
}
}The key insight: boundary lines are reliable, content lines aren't.
┌─────────────────────┐ ← Boundary: LLMs get this right (repetitive)
│ Content here │ ← Content: LLMs mess this up (variable)
│ More content │ ← Content: Same problem
└─────────────────────┘ ← Boundary: LLMs get this right (repetitive)
Algorithm:
- Scan - Find all boundary lines (
┌───┐,└───┘,+---+) - Measure - Record the display width of each boundary line
- Match - For each content line ending with
│or|:- Find a boundary width that's 1-3 characters wider
- This is the "target width" for that line
- Pad - Insert spaces before the right border character
Unicode box-drawing:
┌───────┐ ╔═══════╗
│ Box 1 │ ║ Box 2 ║
└───────┘ ╚═══════╝
ASCII:
+-------+
| Box 3 |
+-------+
Nested boxes:
┌─────────────────┐
│ ┌─────────────┐ │
│ │ Inner │ │
│ └─────────────┘ │
└─────────────────┘
- Tree structures (
├── folder) - Lines without border characters
- Already-aligned diagrams
- Non-diagram code blocks
- Code blocks with
nofixor*-nofixlanguage tag (e.g.,```nofixor```text-nofix)
import { boxfixMarkdown, boxfix } from 'boxfix';
// Process markdown with code blocks
const result = boxfixMarkdown(markdownContent);
console.log(result.fixed); // Fixed content
console.log(result.stats); // { linesFixed, blocksProcessed, diagramsFound }
// Process raw diagram content
const diagram = boxfix(diagramContent);| Function | Description |
|---|---|
boxfixMarkdown(md) |
Process markdown, fixing diagrams in code blocks |
boxfix(content) |
Process raw content (auto-detects if diagram) |
boxfixDiagram(content) |
Process content known to be a diagram |
isDiagram(content) |
Check if content appears to be a diagram |
isBoundaryLine(line) |
Check if line is a box boundary |
isContentLine(line) |
Check if line is box content |
isTreeLine(line) |
Check if line is a tree structure (excluded from processing) |
getDisplayWidth(str) |
Get visual width (handles Unicode, CJK, emoji) |
expandTabs(str) |
Expand tabs to spaces for consistent width calculation |
import type { BoxfixResult, BoxfixStats, CodeBlock } from 'boxfix';
interface BoxfixResult {
fixed: string; // The fixed content
stats: BoxfixStats; // Processing statistics
}
interface BoxfixStats {
linesFixed: number; // Lines that were padded
blocksProcessed: number; // Code blocks examined
diagramsFound: number; // Diagrams detected
}
interface CodeBlock {
raw: string; // Full block including fences
content: string; // Content inside fences
language: string | null; // Language identifier
start: number; // Start position in source
end: number; // End position in source
}# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: boxfix
name: Fix diagram borders
entry: npx boxfix --check
language: system
files: '\.md$'# .github/workflows/diagrams.yml
name: Check Diagrams
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npx boxfix --check **/*.mdThe --hook flag enables seamless integration with AI coding agents. It reads JSON from stdin, extracts the file path from common field patterns, and silently processes markdown files.
Key features:
- Reads JSON payload from stdin (as provided by agent hooks)
- Extracts file path from common JSON structures
- Silently skips non-markdown files
- Always exits 0 to never break agentic workflows
- Works with Claude Code, Cursor, Windsurf, and other agents
Supported JSON formats:
| Format | Example | Used by |
|---|---|---|
tool_input.file_path |
{"tool_input":{"file_path":"..."}} |
Claude Code |
file_path |
{"file_path":"..."} |
Cursor, Windsurf |
filePath |
{"filePath":"..."} |
Generic (camelCase) |
path |
{"path":"..."} |
Minimal |
Automatically fix diagrams as Claude writes them using hooks.
Add to .claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npx boxfix --hook"
}
]
}
]
}
}Cursor 1.7+ supports hooks for agent lifecycle control.
Add to .cursor/hooks.json:
{
"afterFileEdit": [
{
"command": "npx boxfix --hook"
}
]
}Windsurf (Codeium) supports Cascade Hooks for automation.
Add to .windsurf/hooks.json:
{
"post_write_code": [
{
"command": "npx boxfix --hook"
}
]
}OpenCode supports plugins for extensibility. You can use the oh-my-opencode package which provides Claude Code hook compatibility, or create a custom plugin.
For any agent that pipes JSON with a file path to stdin on file edit events, the --hook flag should work out of the box. The tool checks for file paths in common locations (see table above) and silently exits 0 if no valid markdown path is found.
It fixes boxes. That's it.
Current scope: boxfix pads short content lines to match boundary widths. It assumes the boundary lines are correct and adjusts content to fit.
What it doesn't do (yet):
- Expand boundaries when content is longer than the box
- Shrink content that overflows
- Reflow text within boxes
If your diagram has content that overflows the boundaries, you'll need to either:
- Manually widen the boundary lines, or
- Shorten the content
Boundary expansion is planned for a future release.
This entire library was built with Claude Code and Claude Opus 4.5. Every line of code, test, and documentation was generated through AI-assisted development.
MIT