Markdown-First Habit Tracking: A Developer's Guide
As developers, we have a complicated relationship with productivity apps. We build software for a living, so we're acutely aware of the tradeoffs lurking behind every shiny UI. We know what "cloud-synced" really means (their database, their rules). We know what "free tier" really means (you're the product, or you will be).
And yet, we keep trying habit trackers that treat us like non-technical users. Apps that won't give us an API. Apps that store our data in formats we can't inspect. Apps that think "export to CSV" counts as data portability.
There's a better way. And it's embarrassingly simple: markdown files.
The Case for Markdown
If you're a developer, you already know markdown. You write READMEs in it, document APIs with it, take notes in it. It's the lingua franca of technical writing.
Markdown is also, accidentally, a perfect format for structured personal data:
- Human-readable: Open the file, understand the data
- Machine-parseable: Regular expressions, AST parsers, or even simple line-by-line reading
- Version-controllable: Git diffs show exactly what changed and when
- Universal: Every platform, every editor, every tool understands plain text
- Durable: Will be readable in 50 years. No format migration needed.
For habit tracking specifically, markdown gives you checkboxes (- [x]), headers for organization, frontmatter for metadata, and free-form text for notes. It's everything you need.
The Data Format
Here's a habit log format that's both human-friendly and machine-parseable:
---
date: 2026-02-14
done: 4
total: 5
completion: 80
---
# Friday, February 14, 2026
## Habits
- [x] ๐๏ธ Exercise โ Strength Training | 45 min | ๐ฅ 12
- [x] ๐ Read | 30 pages | ๐ฅ 8
- [x] ๐ซ No Alcohol | | ๐ฅ 30
- [x] ๐ป Code | 2 hours | ๐ฅ 5
- [ ] ๐ง Meditate
## Notes
Solid workout today. Hit a PR on deadlifts.
Let's break down the format:
Frontmatter
Standard YAML frontmatter with summary stats. Useful for quick queries without parsing the full document.
Habit Line Format
- [x] ๐๏ธ Exercise โ Strength Training | 45 min | ๐ฅ 12
โ โ โ โ โ โ
โ โ โ โ โ โโ Streak count
โ โ โ โ โโ Value/duration
โ โ โ โโ Flex habit variant
โ โ โโ Habit name
โ โโ Emoji identifier
โโ Checkbox (done/not done)
The pipe-delimited format is a pragmatic choice: easy to parse, easy to read, and doesn't conflict with markdown syntax. The arrow (โ) separates the habit from its flex variant.
Parsing Habit Files in Code
TypeScript/JavaScript
interface HabitEntry {
name: string;
emoji: string;
done: boolean;
variant?: string;
value?: string;
streak?: number;
}
function parseHabitLine(line: string): HabitEntry | null {
const match = line.match(
/^- \[([ x])\] (\S+)\s+(.+?)(?:\s*โ\s*(.+?))?\s*(?:\|\s*(.*?))?\s*(?:\|\s*๐ฅ\s*(\d+))?\s*$/
);
if (!match) return null;
return {
done: match[1] === 'x',
emoji: match[2],
name: match[3].trim(),
variant: match[4]?.trim(),
value: match[5]?.trim() || undefined,
streak: match[6] ? parseInt(match[6]) : undefined,
};
}
function parseHabitFile(content: string): HabitEntry[] {
return content
.split('\n')
.map(parseHabitLine)
.filter((h): h is HabitEntry => h !== null);
}
Python
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
@dataclass
class HabitEntry:
name: str
emoji: str
done: bool
variant: Optional[str] = None
value: Optional[str] = None
streak: Optional[int] = None
HABIT_RE = re.compile(
r'^- \[( |x)\] (\S+)\s+(.+?)(?:\s*โ\s*(.+?))?\s*(?:\|\s*(.*?))?\s*(?:\|\s*๐ฅ\s*(\d+))?\s*$'
)
def parse_habit_line(line: str) -> Optional[HabitEntry]:
m = HABIT_RE.match(line)
if not m:
return None
return HabitEntry(
done=m.group(1) == 'x',
emoji=m.group(2),
name=m.group(3).strip(),
variant=m.group(4).strip() if m.group(4) else None,
value=m.group(5).strip() if m.group(5) else None,
streak=int(m.group(6)) if m.group(6) else None,
)
def parse_habit_file(path: Path) -> list[HabitEntry]:
lines = path.read_text().splitlines()
return [h for line in lines if (h := parse_habit_line(line))]
# Usage
habits = parse_habit_file(Path("habits/logs/2026-02-14.md"))
for h in habits:
status = "โ
" if h.done else "โฌ"
print(f"{status} {h.emoji} {h.name} (streak: {h.streak or 0})")
Shell (for quick queries)
# Count completed habits today
grep -c '\- \[x\]' habits/logs/$(date +%Y-%m-%d).md
# Find your longest streak across all files
grep -oh '๐ฅ [0-9]*' habits/logs/*.md | sort -t' ' -k2 -rn | head -1
# List all days you exercised
grep -l '\[x\].*Exercise' habits/logs/*.md | sort
# Calculate monthly completion rate
awk '/^completion:/{sum+=$2; n++} END{print sum/n "%"}' habits/logs/2026-02-*.md
Building Automations
GitHub Actions: Weekly Habit Report
name: Weekly Habit Report
on:
schedule:
- cron: '0 9 * * 1' # Monday 9am UTC
jobs:
report:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate report
run: |
echo "# Weekly Habit Report" > report.md
echo "## $(date -d '7 days ago' +%Y-%m-%d) to $(date +%Y-%m-%d)" >> report.md
echo "" >> report.md
total=0
done=0
for f in habits/logs/$(date -d '7 days ago' +%Y-%m-%d).md \
habits/logs/$(date -d '6 days ago' +%Y-%m-%d).md \
habits/logs/$(date -d '5 days ago' +%Y-%m-%d).md \
habits/logs/$(date -d '4 days ago' +%Y-%m-%d).md \
habits/logs/$(date -d '3 days ago' +%Y-%m-%d).md \
habits/logs/$(date -d '2 days ago' +%Y-%m-%d).md \
habits/logs/$(date -d '1 day ago' +%Y-%m-%d).md; do
if [ -f "$f" ]; then
t=$(grep -c '\- \[[ x]\]' "$f" || true)
d=$(grep -c '\- \[x\]' "$f" || true)
total=$((total + t))
done=$((done + d))
fi
done
pct=$((done * 100 / total))
echo "**Completion rate**: ${done}/${total} (${pct}%)" >> report.md
cat report.md
Cron Job: Daily Habit Reminder
#!/bin/bash
# daily-habit-check.sh โ run at 9pm via cron
TODAY=$(date +%Y-%m-%d)
FILE="$HOME/vault/habits/logs/${TODAY}.md"
if [ ! -f "$FILE" ]; then
echo "โ ๏ธ No habit log for today!"
# Send notification via your preferred method
exit 0
fi
REMAINING=$(grep -c '\- \[ \]' "$FILE" || true)
if [ "$REMAINING" -gt 0 ]; then
echo "You have ${REMAINING} unchecked habits today."
# Send push notification, email, Slack message, etc.
fi
Raycast/Alfred Script: Quick Habit Toggle
#!/bin/bash
# Toggle a habit in today's file from anywhere
HABIT="$1"
TODAY=$(date +%Y-%m-%d)
FILE="$HOME/vault/habits/logs/${TODAY}.md"
if grep -q "\- \[ \].*${HABIT}" "$FILE"; then
sed -i '' "s/- \[ \] \(.*${HABIT}\)/- [x] \1/" "$FILE"
echo "โ
${HABIT} done!"
elif grep -q "\- \[x\].*${HABIT}" "$FILE"; then
sed -i '' "s/- \[x\] \(.*${HABIT}\)/- [ ] \1/" "$FILE"
echo "โฌ ${HABIT} unchecked"
else
echo "โ Habit not found: ${HABIT}"
fi
API Integration
If you're using a habit tracker with an open API (like Habit Space), you can build integrations that go far beyond what any single app offers.
Fetching Habit Data via API
const API_BASE = 'http://localhost:3000/api';
// Get today's habits
const response = await fetch(`${API_BASE}/habits/today`, {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
const habits = await response.json();
// Toggle a habit
await fetch(`${API_BASE}/habits/toggle`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
habit: 'exercise',
date: '2026-02-14',
done: true,
variant: 'Strength Training',
value: '45 min'
})
});
iOS Shortcuts Integration
With an API, you can create iOS Shortcuts that:
- Toggle habits from your home screen with one tap
- Log habits via Siri ("Hey Siri, log my exercise")
- Get a morning briefing of yesterday's completion rate
- Auto-log habits based on Apple Health data
Integrating with Other Tools
Notion: Sync habit completion to a Notion database for team accountability.
Home Assistant: Trigger smart home actions when habits are completed (green light when all done, reminder on smart speaker for remaining habits).
Slack/Discord: Post daily habit summaries to a channel for accountability.
Grafana: Point Grafana at your habit data for serious dashboards. Since the data is in files, you can write a simple exporter that converts markdown to Prometheus metrics.
# Minimal Prometheus exporter for habit data
from prometheus_client import Gauge, start_http_server
from pathlib import Path
import time
completion = Gauge('habit_completion_pct', 'Daily completion percentage', ['date'])
streak = Gauge('habit_streak', 'Current streak', ['habit'])
def update_metrics():
# Read today's file and update gauges
today = time.strftime('%Y-%m-%d')
path = Path(f'habits/logs/{today}.md')
if path.exists():
habits = parse_habit_file(path)
done = sum(1 for h in habits if h.done)
total = len(habits)
completion.labels(date=today).set(done / total * 100 if total else 0)
for h in habits:
if h.streak:
streak.labels(habit=h.name).set(h.streak)
start_http_server(8000)
while True:
update_metrics()
time.sleep(60)
Git-Based Habit Tracking
Since your habits are files, Git gives you superpowers:
Version History for Free
# See your habit history
git log --oneline habits/logs/
# What did you change today?
git diff habits/logs/2026-02-14.md
# When did you break a streak?
git log -p --all -S '๐ฅ 0' -- habits/logs/
Automated Commits
# .git/hooks/post-save (or via cron)
#!/bin/bash
cd ~/vault
git add habits/
git commit -m "habits: $(date +%Y-%m-%d %H:%M)" --allow-empty-message
Analytics from Git History
# Completion rate over time from git history
for commit in $(git log --format=%H --since="30 days ago" -- habits/logs/); do
date=$(git log -1 --format=%ai $commit | cut -d' ' -f1)
pct=$(git show $commit:habits/logs/${date}.md 2>/dev/null | awk '/^completion:/{print $2}')
[ -n "$pct" ] && echo "$date: $pct%"
done
Building a Habit Dashboard
You can build a simple web dashboard that reads your markdown files:
// pages/api/habits.ts (Next.js API route)
import { readdir, readFile } from 'fs/promises';
import { join } from 'path';
const HABITS_DIR = process.env.HABITS_DIR || join(process.env.HOME!, 'vault/habits/logs');
export default async function handler(req, res) {
const files = await readdir(HABITS_DIR);
const logs = await Promise.all(
files
.filter(f => f.endsWith('.md'))
.sort()
.slice(-30) // Last 30 days
.map(async f => {
const content = await readFile(join(HABITS_DIR, f), 'utf-8');
const habits = parseHabitFile(content);
const date = f.replace('.md', '');
return {
date,
habits,
completion: habits.length > 0
? Math.round(habits.filter(h => h.done).length / habits.length * 100)
: 0
};
})
);
res.json(logs);
}
Then render it however you want โ a heatmap with D3, a chart with Chart.js, or a terminal dashboard with blessed.
The Developer's Habit Stack
Here's a complete, opinionated stack for developer habit tracking:
| Layer | Tool | Role | |-------|------|------| | Storage | Markdown files | Source of truth | | Editor | Obsidian + Habit Space plugin | Daily interaction | | Sync | Git or Syncthing | Cross-device | | API | Habit Space API | Programmatic access | | Automation | Cron + shell scripts | Reminders, reports | | Analytics | Dataview or custom dashboard | Visualization | | Mobile | Habit Space iOS app | On-the-go tracking | | Backup | Git remote (private repo) | History + backup |
Every layer is replaceable. Swap Obsidian for VS Code. Swap Git for iCloud. Swap the dashboard for a Grafana instance. The markdown files don't care.
Why This Matters
As developers, we build systems for a living. We know the value of loose coupling, open standards, and data portability. We preach these principles in our code.
But when it comes to personal tools, we often compromise. We use apps that lock our data in proprietary formats, that require accounts and subscriptions, that could disappear tomorrow.
Habit tracking is a long game. You're building a dataset that becomes more valuable over time. The system you choose today needs to work in 5 years, 10 years, or longer. Markdown will still be markdown in 2036. That SaaS app you're eyeing? Maybe. Maybe not.
Build your habit system like you'd build software: with open formats, clear interfaces, and the ability to swap any component without losing data.
Your future self โ the one looking at a decade of habit data, finding patterns you can't even imagine yet โ will thank you.
Getting Started
- Create a
habits/logs/directory somewhere you control - Create today's file:
2026-02-14.md - Add some checkboxes
- Check them off
- Commit to Git
- Repeat tomorrow
Everything else โ plugins, APIs, automations, dashboards โ is optional. The core system is just files and checkboxes. Start there.
mkdir -p ~/vault/habits/logs
cat > ~/vault/habits/logs/$(date +%Y-%m-%d).md << 'EOF'
---
date: $(date +%Y-%m-%d)
---
# $(date +"%A, %B %d, %Y")
## Habits
- [ ] ๐๏ธ Exercise
- [ ] ๐ Read
- [ ] ๐ป Code on a side project
EOF
echo "Done. Now go check some boxes."