Workflow CLI

CLI-First AI Development

You live in the terminal. tmux, vim, grep, pipes. AI should fit into that world — not pull you out of it. This guide covers everything from Claude Code to shell-native AI patterns.


Part 1: Why CLI-First

Most AI coding tool demos show a GUI: a sidebar chat panel, inline suggestions appearing in an editor, a button you click to accept changes. That workflow is fine for developers who work in VS Code or Cursor all day. But if your daily environment looks more like this —

$ tmux new-session -s work
$ vim src/api/routes.ts
$ npm test -- --watch
$ git log --oneline -10
$ ssh prod-server 'tail -f /var/log/app.log'

— then GUI-based AI tools are a context switch, not a productivity boost. You have to leave your flow, open a browser or a different editor, copy-paste code back and forth, and then return to where you were.

CLI-first AI development keeps you in the terminal. The advantages are significant:

The Core Principle

The best AI tool is the one that integrates into your existing workflow, not the one that replaces it. If your workflow is terminal-based, your AI tools should be terminal-based.


Part 2: Claude Code in Depth

Claude Code is Anthropic's CLI tool for AI-assisted development. It runs in your terminal, reads your project files, executes commands, and makes edits — all through a conversational interface that feels like pair programming with someone who can actually touch the codebase.

Installation and Setup

# Install globally via npm
$ npm install -g @anthropic-ai/claude-code

# Verify installation
$ claude --version

# Start an interactive session in your project directory
$ cd your-project
$ claude

Claude Code needs either a Claude Pro/Max subscription or an Anthropic API key. On first launch it walks you through authentication.

The CLAUDE.md File

This is the most important file in your project for CLI-first development. Create a CLAUDE.md in your project root, and Claude Code reads it automatically at the start of every session. It's your project's AI instruction manual.

# Project: TaskFlow API

## Stack
- TypeScript, Express, SQLite (better-sqlite3)
- Vitest for testing
- Zod for validation

## Conventions
- All async functions use Result<T, AppError> return type
- Database access through repository classes only, never direct queries
- Error codes: DOMAIN_ACTION_REASON (e.g. USER_CREATE_EMAIL_TAKEN)
- API responses: { data: T } on success, { error: string, code: string } on failure

## Commands
- `npm test` — run all tests
- `npm run test:watch` — run tests in watch mode
- `npm run lint` — run ESLint
- `npm run build` — TypeScript compilation

## Architecture
- src/routes/ — Express route handlers
- src/repos/ — Repository classes (one per table)
- src/middleware/ — Auth, validation, error handling
- src/types/ — Shared TypeScript types
- tests/ — Mirrors src/ structure

## Rules
- Never use `any` type
- Always validate inputs with Zod at route level
- All database queries must be parameterized
- Tests must cover happy path + at least one error case

Every session starts with Claude Code reading this file. You never re-explain your stack, your conventions, or your project structure. Write it once, benefit every session.

Pro Tip: Evolve CLAUDE.md Over Time

When Claude Code makes a mistake that your CLAUDE.md should have prevented — wrong naming convention, wrong error pattern, wrong test structure — add a rule to the file immediately. After a few weeks, your CLAUDE.md becomes a comprehensive project guide that prevents nearly all common AI errors. It's also valuable documentation for human team members.

The Iterative Loop

Claude Code's defining feature is the ability to take action, verify the result, and adjust. This is the loop that makes it reliable for real work:

You

Add rate limiting to all auth endpoints. Use express-rate-limit. Max 5 attempts per IP per 15 minutes on login and register. Add tests.

Claude Code

I'll add rate limiting to the auth routes. Let me:

1. Install express-rate-limit
2. Create the rate limiter middleware
3. Apply it to login and register routes
4. Write tests
5. Run the test suite to verify

Claude Code then executes each step: runs npm install, edits the route files, creates test files, and runs npm test. If tests fail, it reads the output, fixes the issue, and runs again. This loop — edit → run → read output → fix → run — continues until tests pass.

This is fundamentally different from a chat interface where you copy-paste code back and forth. Claude Code closes the feedback loop because it can verify its own work.

Practical Session Patterns

# Start a session and make a focused change
$ claude "Add input validation to all POST endpoints using Zod"

# Ask it to explore before acting
$ claude "Review src/routes/tasks.ts and list potential bugs. Don't fix yet."

# Multi-step work
$ claude "Refactor the user repository to use prepared statements,
         update all routes that use it, and run tests after each change"

# Quick one-shot
$ claude "What does the function on line 47 of src/utils/auth.ts do?"

# Use with a specific file
$ claude "Add JSDoc to all exported functions in src/repos/userRepo.ts"

Slash Commands

Inside an interactive Claude Code session, several slash commands control behavior:

⚠ Watch Your Context Window

In long sessions, Claude Code's context window fills up. When that happens, responses degrade — the same context drift problem from the manual's field guide, but in the terminal. Use /compact proactively when switching tasks, and start new sessions for unrelated work.


Part 3: Aider for Open-Source Workflows

Aider is an open-source terminal AI coding tool. Where Claude Code is Anthropic's integrated product, Aider is a bring-your-own-model tool with strong git integration and a transparent codebase you can inspect and modify.

Setup

# Install via pip
$ pip install aider-chat

# Configure with Claude (recommended)
$ export ANTHROPIC_API_KEY=sk-ant-...

# Or with OpenAI
$ export OPENAI_API_KEY=sk-...

# Start in your project directory
$ cd your-project
$ aider

The Git-First Approach

Aider's distinctive feature is its tight git integration. Every change Aider makes becomes a git commit automatically. This means:

$ aider src/routes/auth.ts src/middleware/rateLimit.ts

aider> Add rate limiting middleware and apply it to the login route.

# Aider edits both files and auto-commits:
# "feat: Add rate limiting to login route"

$ git log --oneline -3
a4f2c1d feat: Add rate limiting to login route
b8e3d2a refactor: Extract auth helpers
c1a4f5b fix: Handle null user in profile endpoint

Adding Files to Context

You specify which files Aider can read and edit by adding them at launch or during the session. This explicit file selection is both a feature and a safety mechanism — Aider only touches files you've approved.

# Start with specific files
$ aider src/routes/auth.ts src/types/user.ts

# Add more files during session
aider> /add src/middleware/auth.ts

# Add read-only files for context (won't be edited)
aider> /read-only src/types/shared.ts

# Drop files from the session
aider> /drop src/types/user.ts

When to Use Aider vs Claude Code


Part 4: Shell Patterns

Beyond dedicated tools, you can integrate AI directly into your shell workflow using the Anthropic API, simple scripts, and Unix pipes. These patterns are lightweight, composable, and work anywhere you have curl.

The Basic Shell Helper

# Quick AI query from anywhere in the terminal
ask() {
  curl -s https://api.anthropic.com/v1/messages \
    -H "x-api-key: $ANTHROPIC_API_KEY" \
    -H "content-type: application/json" \
    -H "anthropic-version: 2023-06-01" \
    -d "{
      \"model\": \"claude-sonnet-4-20250514\",
      \"max_tokens\": 1024,
      \"messages\": [{\"role\": \"user\", \"content\": \"$*\"}]
    }" | jq -r '.content[0].text'
}
# Quick questions without leaving the terminal
$ ask "What's the difference between useMemo and useCallback in React?"

# Explain an error
$ ask "Explain this error: ECONNREFUSED 127.0.0.1:5432"

# Generate a command
$ ask "Give me a find command that deletes all .log files older than 7 days"

Piping Code to AI

The real power of shell-based AI is composition with pipes. Send code directly to AI for analysis, review, or transformation:

# Review a specific file
$ cat src/auth.ts | ask "Review this code for security issues"

# Explain a complex function
$ sed -n '45,90p' src/utils/parser.ts | ask "Explain what this function does"

# Generate tests for a module
$ cat src/repos/userRepo.ts | ask "Write Vitest tests for all public methods"

# Find and review recent changes
$ git diff HEAD~3 | ask "Summarize these changes and flag any concerns"

# Analyze error logs
$ tail -50 /var/log/app.log | ask "What errors are recurring? Suggest fixes."

The Code Review Alias

# AI review of staged changes before commit
ai-review() {
  local diff=$(git diff --cached)
  if [ -z "$diff" ]; then
    echo "No staged changes to review."
    return 1
  fi
  echo "$diff" | ask "Review this git diff. Check for:
    1. Bugs or logic errors
    2. Security issues
    3. Missing error handling
    4. Naming inconsistencies
    Be concise. Only mention real issues."
}
$ git add -p                 # Stage changes interactively
$ ai-review                  # AI reviews before you commit
$ git commit -m "feat: ..."  # Commit with confidence

Batch Processing with find + xargs

# Generate documentation for undocumented files
$ find src/routes -name '*.ts' -exec sh -c '
  if ! grep -q "/**" "$1"; then
    echo "Documenting: $1"
    cat "$1" | ask "Add JSDoc to all exported functions. Return the complete file." > "$1.tmp"
    mv "$1.tmp" "$1"
  fi
' _ {} \;
⚠ Always Review Batch Output

Shell scripts that pipe files through AI and write results back are powerful but dangerous. Always review the output before committing. A safer pattern: write to .tmp files first, diff them against originals, then replace only after manual review. Or work on a branch and review the full diff before merging.


Part 5: The Terminal Workflow

Here's how a full CLI-first AI development session actually looks. This is the layout, the flow, and the habits that make it productive.

The tmux Layout

# Create a development session
$ tmux new-session -s dev

# Split into three panes:
# ┌─────────────────┬──────────────────┐
# │                 │                  │
# │   Editor        │   Claude Code    │
# │   (vim/nvim)    │   or Aider       │
# │                 │                  │
# ├─────────────────┴──────────────────┤
# │   Tests / Shell / Logs             │
# └────────────────────────────────────┘

# Pane 0: Editor
$ vim src/routes/tasks.ts

# Pane 1: AI assistant
$ claude   # or: aider src/routes/tasks.ts

# Pane 2: Tests in watch mode
$ npm test -- --watch

Three panes, three roles: you edit in one, AI works in another, tests run continuously in the third. When AI makes a change, you see tests pass or fail immediately in the bottom pane. When you make a manual edit, same thing.

A Typical Development Flow

Here's 30 minutes of real CLI-first development on a feature:

0:00
Start session
tmux layout open. Claude Code running. Tests watching. Open the relevant files in vim.
0:02
Design the feature
In the Claude Code pane: "I need to add task assignment. A task can be assigned to a user. Describe the schema change, API changes, and frontend changes needed." Read the plan, adjust if needed.
0:05
Generate the migration
"Create the SQLite migration to add an assignee_id column to tasks with a foreign key to users." Claude Code writes the migration file and runs it.
0:08
Update the API
"Update the task routes to support assigning users. Add a PATCH /tasks/:id/assign endpoint." Claude Code edits the route file and repo. Tests in the bottom pane start failing — expected, since test data doesn't include the new column yet.
0:12
Fix tests
"Update the test fixtures and add tests for the new assign endpoint." Claude Code updates test helpers and adds new test cases. Tests go green.
0:15
Manual review
Switch to vim pane. Review the changed files. Check the migration, the route handler, the validation. Make a small naming adjustment manually.
0:20
Edge cases
"What happens if you assign a task to a user who doesn't exist? Or assign to the same user twice? Add tests for these edge cases." Claude Code adds the tests — one fails, revealing a missing check. It fixes the route handler. Tests go green.
0:25
Review and commit
git diff to review everything. ai-review alias for a final check. git add -A && git commit.
0:28
Done
Schema change, API endpoint, validation, edge case handling, full test coverage. 28 minutes. Never left the terminal.
The Flow State Advantage

The entire session happens in one terminal window. No browser tabs, no file uploads, no copy-paste between applications. Your hands stay on the keyboard, your eyes stay on code, and the feedback loop between "ask AI" and "see tests pass" is measured in seconds, not minutes.


Part 6: Automation Scripts

The ultimate expression of CLI-first AI development: scripts that run AI operations without you being present. These are the building blocks for integrating AI into your development pipeline.

Pre-Commit AI Review

#!/bin/bash
# AI review on every commit — catches issues before they land

DIFF=$(git diff --cached --diff-filter=ACMR)
[ -z "$DIFF" ] && exit 0

REVIEW=$(echo "$DIFF" | curl -s https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "content-type: application/json" \
  -H "anthropic-version: 2023-06-01" \
  -d @- << EOF | jq -r '.content[0].text'
{
  "model": "claude-sonnet-4-20250514",
  "max_tokens": 1024,
  "messages": [{
    "role": "user",
    "content": "Review this diff for bugs, security issues, and obvious mistakes. If everything looks fine, respond with just 'LGTM'. If there are issues, list them briefly.\n\n$(echo "$DIFF" | jq -Rs .)"
  }]
}
EOF
)

if echo "$REVIEW" | grep -qi "LGTM"; then
  echo "✓ AI review: looks good"
  exit 0
else
  echo "⚠ AI review found issues:"
  echo "$REVIEW"
  echo ""
  read -p "Commit anyway? (y/N) " -n 1 -r
  echo
  [[ $REPLY =~ ^[Yy]$ ]] && exit 0 || exit 1
fi

Changelog Generator

#!/bin/bash
# Generate changelog entry from recent commits

SINCE=${1:-"last week"}
COMMITS=$(git log --since="$SINCE" --pretty=format:"%h %s" --no-merges)

if [ -z "$COMMITS" ]; then
  echo "No commits since $SINCE"
  exit 0
fi

ask "Generate a concise changelog from these git commits.
Group by: Added, Changed, Fixed, Removed.
Skip trivial commits (typos, formatting).
Use past tense. One line per item.

Commits:
$COMMITS"
$ ./scripts/changelog.sh "2 weeks ago"

## Changes (Feb 10 — Feb 24)

### Added
- Rate limiting on authentication endpoints
- Task assignment with user validation
- Batch export endpoint for project data

### Changed
- Migrated user repository to prepared statements
- Updated error codes to DOMAIN_ACTION_REASON format

### Fixed
- Null pointer when accessing archived project tasks
- Race condition in concurrent task position updates

Test Scaffolding

#!/bin/bash
# Generate test file for a source file that doesn't have one

SRC_FILE=$1
if [ -z "$SRC_FILE" ]; then
  echo "Usage: gen-tests.sh <source-file>"
  exit 1
fi

# Derive test file path
TEST_FILE=$(echo "$SRC_FILE" | sed 's|src/|tests/|' | sed 's|\.ts$|.test.ts|')

if [ -f "$TEST_FILE" ]; then
  echo "Test file already exists: $TEST_FILE"
  exit 1
fi

mkdir -p $(dirname "$TEST_FILE")

cat "$SRC_FILE" | ask "Write Vitest tests for this TypeScript module.
Cover: happy path, error cases, edge cases.
Use describe/it blocks. Mock external dependencies.
Import from the relative path to the source file.
Return only the test file contents, no explanation." > "$TEST_FILE"

echo "Created: $TEST_FILE"
echo "Run: npx vitest $TEST_FILE"
$ ./scripts/gen-tests.sh src/repos/taskRepo.ts
Created: tests/repos/taskRepo.test.ts
Run: npx vitest tests/repos/taskRepo.test.ts

$ npx vitest tests/repos/taskRepo.test.ts
 ✓ tests/repos/taskRepo.test.ts (8 tests)
   ✓ TaskRepo > create > creates a task with valid input
   ✓ TaskRepo > create > rejects missing title
   ✓ TaskRepo > findByProject > returns tasks for project
   ✓ TaskRepo > findByProject > returns empty for unknown project
   ...

Batch Documentation

#!/bin/bash
# Find and document all undocumented exported functions

echo "Scanning for undocumented exports..."
COUNT=0

for file in $(find src -name '*.ts' -not -path '*/node_modules/*'); do
  # Check if file has exports without JSDoc
  if grep -q '^export' "$file" && ! grep -q '/\*\*' "$file"; then
    echo "  Documenting: $file"
    
    DOCUMENTED=$(cat "$file" | ask "Add JSDoc comments to all exported functions and types.
Keep all existing code exactly as-is. Only add documentation.
Return the complete file.")
    
    echo "$DOCUMENTED" > "${file}.documented"
    COUNT=$((COUNT + 1))
  fi
done

echo ""
echo "Documented $COUNT files. Review with:"
echo "  diff src/routes/tasks.ts src/routes/tasks.ts.documented"
echo ""
echo "Apply with:"
echo "  for f in \$(find src -name '*.documented'); do mv \"\$f\" \"\${f%.documented}\"; done"

Pro Tip: The .documented Pattern

Notice the script writes to .documented files instead of overwriting originals. This is the safe pattern for batch AI operations: generate to temporary files, review the diffs, then apply. Never let a batch script overwrite source files directly — one hallucinated response can corrupt a file.


Getting Started

You don't need to set up everything at once. Start with what fits your current workflow:

1

Add the ask function to your shell

Five lines in your .bashrc. Gives you instant AI access from anywhere in the terminal. Use it for a week.

2

Try Claude Code on a real task

Install it, create a CLAUDE.md, and use it for one feature. The iterative test loop will either click or it won't — but you need to try it on real work to know.

3

Set up the tmux layout

Editor + AI + tests. Three panes. Use it for a full development session. The flow state difference is immediate.

4

Add one automation script

The pre-commit review hook or the test generator — whichever solves a real annoyance in your current workflow.


CLI-First AI Development — Summary

Back to Home