Journalot – Building a Git-Backed Journaling CLI That Developers Actually Use

Written by jtaylortech | Published 2025/10/15
Tech Story Tags: git | cli | markdown | productivity | terminal | journaling | lifestyle | self-care

TLDRJournalot is terminal-based, git-backed, zero friction. No accounts, no sync services, just markdown files in a git repo. Works on Mac/Linux. MIT licensed.via the TL;DR App

Every journaling app I tried either suffered from feature bloat or I'd simply forget to use it. After trying and abandoning several apps, I realized the problem wasn't motivation; it was friction. So I built journalot, a terminal-based journaling tool that leverages git for version control and syncing. Here's how it works under the hood.

The Core Problem: Reducing Friction to Zero

The biggest barrier to digital journaling isn't lack of discipline, it's context switching. Opening a separate app, waiting for sync, dealing with unfamiliar keybindings. Each friction point compounds.

My solution: meet developers where they already are. In the terminal, with their preferred editor, using tools they already trust.

Implementation Details

1. Smart Editor Detection

Rather than forcing a specific editor, journalot follows a priority chain:

get_editor() {
    if [ -n "$EDITOR" ]; then
        echo "$EDITOR"
    elif command -v code &> /dev/null; then
        echo "code"
    elif command -v vim &> /dev/null; then
        echo "vim"
    elif command -v nano &> /dev/null; then
        echo "nano"
    else
        error_exit "No suitable editor found. Please set EDITOR environment variable."
    fi
}

This respects the user's $EDITOR environment variable first (standard Unix convention), then falls back to common editors in order of popularity. No configuration required for 90% of users.

2. Change Detection: Only Commit When Necessary

The biggest technical challenge was detecting whether the user actually wrote anything. Simply opening and closing an editor shouldn't create a commit.

# Capture file hash before editing
BEFORE_HASH=$(md5sum "$FILENAME" 2>/dev/null || md5 -q "$FILENAME" 2>/dev/null)

# Open editor (blocks until closed)
$EDITOR_CMD "$FILENAME"

# Check if content changed
AFTER_HASH=$(md5sum "$FILENAME" 2>/dev/null || md5 -q "$FILENAME" 2>/dev/null)

if [ "$BEFORE_HASH" != "$AFTER_HASH" ]; then
    # Only prompt for commit if file was modified
    git add "$FILENAME"
    git commit -m "Journal entry for $ENTRY_DATE"
fi

This uses MD5 hashing to detect actual changes. The dual command syntax (md5sum for Linux, md5 -q for macOS) ensures cross-platform compatibility without external dependencies.

3. Quick Capture: Appending Without Opening an Editor

For fleeting thoughts, opening an editor is still too much friction. The quick capture feature lets you append directly:

journal "Had a breakthrough on the authentication bug"

Implementation:

quick_capture() {
    local text="$1"
    local timestamp=$(date '+%H:%M')
    local filename="$JOURNAL_DIR/entries/$ENTRY_DATE.md"

    # Create file with header if it doesn't exist
    if [ ! -f "$filename" ]; then
        echo "# $ENTRY_DATE" > "$filename"
        echo "" >> "$filename"
    fi

    # Append timestamped entry
    echo "" >> "$filename"
    echo "**$timestamp** - $text" >> "$filename"
}

This creates the file if needed, ensures proper markdown formatting, and timestamps each quick capture. Multiple quick captures on the same day append to the same file.

4. Git-Based Sync: Cross-Device Without a Backend

Instead of building a sync service, journalot leverages git. Every journal entry is a commit. Syncing across devices is just git push and git pull.

Auto-sync on entry open:

if git remote get-url origin &> /dev/null; then
    echo "Syncing with remote..."
    if ! git pull origin main --rebase 2>/dev/null; then
        warn_msg "Failed to pull from remote. Continuing with local changes..."
    fi
fi

This pulls before opening an entry to minimize conflicts. Using --rebase keeps history linear. If the pull fails (no internet, conflicts), it warns but continues; offline-first by default.

For push, there's an optional AUTOSYNC=true config flag. Without it, you're prompted after saving:

if [ "$AUTOSYNC" = "true" ]; then
    git add "$FILENAME"
    git commit -m "Journal entry for $ENTRY_DATE"
    git push origin main
else
    echo -n "Commit changes? (y/n): "
    read -r commit_response
    # ... prompt for commit and push
fi

5. Search: Just Grep

No indexing. No database. Just grep:

search_entries() {
    local query="$1"
    grep -i -n -H "$query" "$JOURNAL_DIR/entries"/*.md 2>/dev/null | while IFS=: read -r file line content; do
        local filename=$(basename "$file")
        echo "$filename:$line: $content"
    done
}

grep -i for case-insensitive search, -n for line numbers, -H to show filenames. Results are formatted as filename:line: content, familiar to anyone who's used grep in a codebase.

For 1000+ entries, this is still near-instant on modern hardware. And since entries are dated (YYYY-MM-DD.md), you can narrow searches: grep "bug fix" entries/2025-*.md.

6. Date Parsing for Flexibility

Opening yesterday's entry is a single flag:

journal --yesterday

Implementation handles macOS/Linux differences:

# macOS uses -v flag, Linux uses -d
ENTRY_DATE=$(date -v-1d '+%Y-%m-%d' 2>/dev/null || date -d 'yesterday' '+%Y-%m-%d' 2>/dev/null)

The 2>/dev/null silences errors from the wrong syntax, letting the || fallback succeed. For specific dates:

journal --date 2025-01-15

This just sets ENTRY_DATE to the provided string. Date validation happens naturally; if the date format is wrong, the filename is malformed and the user notices immediately.

Architecture Decisions

Why Bash?

  1. Zero dependencies: Works on any Unix-like system with git
  2. No installation friction: Just copy to /usr/local/bin
  3. Transparent: Users can read the entire implementation in 638 lines
  4. Fast startup: No runtime initialization, no package loading

Why Plain Markdown?

  1. Longevity: Markdown will outlive any proprietary format
  2. Composability: Pipe to other tools (grep, wc, sed)
  3. Portability: Open in any editor, render on GitHub, convert with pandoc
  4. Version control: Git diffs on plain text work beautifully

File Structure

~/journalot/
├── entries/
│   ├── 2025-01-01.md
│   ├── 2025-01-02.md
│   └── 2025-01-03.md
└── .git/

Each entry is a separate file named by date (YYYY-MM-DD.md). This means:

  • Easy to locate any day's entry
  • ls shows your journaling frequency at a glance
  • Week/month views are just shell globs
  • Archive old years by moving files

My Results After 9 Months

I've maintained a daily journaling habit for the first time (aside from my physical journal). The key metrics:

  • Average time to first word written: ~3 seconds (type journal, hit enter, start typing)
  • Entries missed: ~15 days out of 270 (mostly travel without laptop)
  • Total words written: 47,000+

What made it stick:

  1. Zero cognitive load: No app to open, no account to log into, no "should I journal?" friction
  2. Editor mastery: Vim keybindings, spell-check, snippets: everything I've already trained my fingers for
  3. Trust in durability: Git history means nothing is lost, entries are future-proof plain text

Try It

git clone https://github.com/jtaylortech/journalot.git
cd journalot
sudo ./install.sh
journal

GitHub: https://github.com/jtaylortech/journalot

The entire codebase is 638 lines of bash, MIT licensed. No telemetry, no accounts, no cloud lock-in.


What I'm adding next (feedback welcome):

  1. End-to-end encryption for git remotes (currently relies on repo privacy)
  2. Conflict resolution helper for simultaneous edits on multiple devices
  3. iOS companion app with SSH git push (for journaling without a laptop)

What would make this more useful for you?


Written by jtaylortech | Engineer @ AWS with expertise in Cloud infrastructure. USAF Veteran.
Published by HackerNoon on 2025/10/15