๐Ÿš• Taxi Meter โ€” Configure

Set your rate. Get an install command. Paste it in your project.

๐Ÿš• 12m30s $31.25 ($150/hr)
preview โ€” 12 min 30 sec session
your statusline will show:
~/my-project | ๐Ÿš• 12m30s $31.25 ($150/hr)
Hourly rate $150/hr
/hr

Install command

bash <(curl -sL https://raw.githubusercontent.com/ideabrian/taxi-meter/main/install.sh) --rate 150
๐Ÿ‘€ View install.sh source โ€” see exactly what runs
#!/bin/bash
# Taxi Meter โ€” project billing timer for Claude Code
# Installs into the current project's .claude/ directory
#
# Usage: curl -sL <raw-url>/install.sh | bash
#    or: bash install.sh [project-dir]

set -e

# --- Parse flags ---
RATE=150
PROJECT=""
while [ $# -gt 0 ]; do
  case "$1" in
    --rate) RATE="$2"; shift 2 ;;
    --rate=*) RATE="${1#*=}"; shift ;;
    *) [ -z "$PROJECT" ] && PROJECT="$1"; shift ;;
  esac
done
PROJECT="${PROJECT:-.}"
PROJECT=$(cd "$PROJECT" && pwd)
CLAUDE_DIR="$PROJECT/.claude"

echo "๐Ÿš• Installing taxi meter into $PROJECT"

# --- 0. Backup existing files we might modify ---
BACKUP_DIR="$CLAUDE_DIR/taxi-backup"
mkdir -p "$BACKUP_DIR"
BACKED_UP=""
for f in settings.json statusline.json .gitignore; do
  if [ -f "$CLAUDE_DIR/$f" ]; then
    cp "$CLAUDE_DIR/$f" "$BACKUP_DIR/$f"
    BACKED_UP="$BACKED_UP $f"
  fi
done
if [ -n "$BACKED_UP" ]; then
  echo "  โœ“ Backed up existing files:$BACKED_UP"
  echo "    (stored in .claude/taxi-backup/)"
fi

# --- 1. Session timer hook ---
mkdir -p "$CLAUDE_DIR/hooks"
cat > "$CLAUDE_DIR/hooks/taxi-start.sh" << 'HOOK'
#!/bin/bash
# Taxi meter โ€” create/reset session timer on start
SID="${CLAUDE_CODE_SESSION_ID:-unknown}"
DIR="$(pwd)/.claude/sessions"
TIMER="$DIR/$SID.json"

mkdir -p "$DIR"

if [ -f "$TIMER" ] && command -v python3 &>/dev/null; then
  python3 -c "
import json, time
with open('$TIMER') as f:
    timer = json.load(f)
timer['start'] = int(time.time())
with open('$TIMER', 'w') as f:
    json.dump(timer, f, indent=2)
"
  echo '{"result":"โฑ Taxi meter reset (resumed)"}'
else
  python3 -c "
import json, time
timer = {
    'start': int(time.time()),
    'session_id': '$SID',
    'completions': []
}
with open('$TIMER', 'w') as f:
    json.dump(timer, f, indent=2)
"
  echo '{"result":"โฑ Taxi meter started"}'
fi
HOOK
chmod +x "$CLAUDE_DIR/hooks/taxi-start.sh"

# --- 2. Register hook in settings ---
SETTINGS="$CLAUDE_DIR/settings.json"
if [ -f "$SETTINGS" ] && command -v jq &>/dev/null; then
  HAS_TAXI=$(jq '[.hooks.SessionStart[]?.hooks[]?.command // empty]
    | any(contains("taxi-start"))' "$SETTINGS" 2>/dev/null)
  if [ "$HAS_TAXI" = "true" ]; then
    echo "  โœ“ settings.json already has taxi hook"
  elif jq -e '.hooks.SessionStart' "$SETTINGS" &>/dev/null; then
    jq '.hooks.SessionStart += [{"hooks":[{"type":"command",
      "command":"bash .claude/hooks/taxi-start.sh"}]}]' \
      "$SETTINGS" > "$SETTINGS.tmp" && mv "$SETTINGS.tmp" "$SETTINGS"
    echo "  โœ“ Appended taxi hook to existing SessionStart hooks"
  else
    jq '.hooks.SessionStart = [{"hooks":[{"type":"command",
      "command":"bash .claude/hooks/taxi-start.sh"}]}]' \
      "$SETTINGS" > "$SETTINGS.tmp" && mv "$SETTINGS.tmp" "$SETTINGS"
    echo "  โœ“ Added SessionStart hook to settings.json"
  fi
else
  mkdir -p "$CLAUDE_DIR"
  cat > "$SETTINGS" << 'SETTINGS_JSON'
{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/taxi-start.sh"
          }
        ]
      }
    ]
  }
}
SETTINGS_JSON
  echo "  โœ“ Created settings.json with SessionStart hook"
fi

# --- 3. Statusline config ---
SL_CONFIG="$CLAUDE_DIR/statusline.json"
if [ ! -f "$SL_CONFIG" ]; then
  echo '{"modules":["taxi"]}' > "$SL_CONFIG"
  echo "  โœ“ Created statusline.json with taxi module"
elif command -v jq &>/dev/null; then
  HAS_TAXI=$(jq '.modules // [] | index("taxi")' "$SL_CONFIG" 2>/dev/null)
  if [ "$HAS_TAXI" = "null" ]; then
    jq '.modules = (.modules // []) + ["taxi"]' "$SL_CONFIG" \
      > "$SL_CONFIG.tmp" && mv "$SL_CONFIG.tmp" "$SL_CONFIG"
    echo "  โœ“ Added taxi to existing statusline.json modules"
  else
    echo "  โœ“ statusline.json already has taxi module"
  fi
else
  echo "  โš  statusline.json exists but jq missing โ€” add \"taxi\" to modules manually"
fi

# --- 4. Write rate config ---
RATE_PER_MIN=$(python3 -c "print(round($RATE / 60, 4))")
echo "{\"rate\": $RATE, \"rate_per_min\": $RATE_PER_MIN}" \
  > "$CLAUDE_DIR/taxi-config.json"
echo "  โœ“ Set rate to \$$RATE/hr (\$$RATE_PER_MIN/min)"

# --- 5. Gitignore sessions ---
GITIGNORE="$CLAUDE_DIR/.gitignore"
if [ -f "$GITIGNORE" ]; then
  grep -q "sessions/" "$GITIGNORE" 2>/dev/null || echo "sessions/" >> "$GITIGNORE"
  grep -q "taxi-backup/" "$GITIGNORE" 2>/dev/null || echo "taxi-backup/" >> "$GITIGNORE"
else
  printf "sessions/\ntaxi-backup/\n" > "$GITIGNORE"
fi
echo "  โœ“ Added sessions/, taxi-backup/ to .claude/.gitignore"

echo ""
echo "๐Ÿš• Taxi meter installed! Restart Claude Code to activate."
echo "   Rate: \$$RATE/hr (\$$RATE_PER_MIN/min)"

What this touches

Downloaded from repo
  • .claude/hooks/taxi-start.sh โ€” the timer hook
Created locally
  • .claude/taxi-config.json โ€” your rate
  • .claude/statusline.json โ€” enables taxi module
  • .claude/sessions/ โ€” timer data (gitignored)
Modified (if they exist)
  • .claude/settings.json โ€” appends SessionStart hook
  • .claude/statusline.json โ€” adds "taxi" to modules
  • .claude/.gitignore โ€” adds sessions/ line
Originals are saved to .claude/taxi-backup/ before any changes. Uninstall restores them exactly.

Uninstall

Restores your original files from backup, removes everything taxi-meter added.

bash <(curl -sL https://raw.githubusercontent.com/ideabrian/taxi-meter/main/uninstall.sh)
๐Ÿ‘€ View uninstall.sh source โ€” see exactly what runs
#!/bin/bash
# Taxi Meter โ€” clean uninstall
# Removes only what install.sh added. Does not delete .claude/ itself.
#
# Usage: bash uninstall.sh [project-dir]

set -e

PROJECT="${1:-.}"
PROJECT=$(cd "$PROJECT" && pwd)
CLAUDE_DIR="$PROJECT/.claude"

echo "๐Ÿš• Uninstalling taxi meter from $PROJECT"
echo ""

# --- 1. Downloaded files ---
echo "  Removing downloaded files:"
if [ -f "$CLAUDE_DIR/hooks/taxi-start.sh" ]; then
  rm "$CLAUDE_DIR/hooks/taxi-start.sh"
  echo "    โœ“ .claude/hooks/taxi-start.sh"
else
  echo "    ยท .claude/hooks/taxi-start.sh (not found)"
fi

# --- 2. Created files ---
echo "  Removing created files:"
if [ -f "$CLAUDE_DIR/taxi-config.json" ]; then
  rm "$CLAUDE_DIR/taxi-config.json"
  echo "    โœ“ .claude/taxi-config.json"
else
  echo "    ยท .claude/taxi-config.json (not found)"
fi

if [ -f "$CLAUDE_DIR/statusline.json" ]; then
  rm "$CLAUDE_DIR/statusline.json"
  echo "    โœ“ .claude/statusline.json"
else
  echo "    ยท .claude/statusline.json (not found)"
fi

if [ -d "$CLAUDE_DIR/sessions" ]; then
  rm -rf "$CLAUDE_DIR/sessions"
  echo "    โœ“ .claude/sessions/"
else
  echo "    ยท .claude/sessions/ (not found)"
fi

# --- 3. Restore or surgically clean modified files ---
BACKUP_DIR="$CLAUDE_DIR/taxi-backup"

echo "  Restoring modified files:"

SETTINGS="$CLAUDE_DIR/settings.json"
if [ -f "$BACKUP_DIR/settings.json" ]; then
  cp "$BACKUP_DIR/settings.json" "$SETTINGS"
  echo "    โœ“ .claude/settings.json (restored from backup)"
elif [ -f "$SETTINGS" ] && command -v jq &>/dev/null; then
  HAS_TAXI=$(jq '.hooks.SessionStart[]?.hooks[]?
    | select(.command | contains("taxi-start"))' "$SETTINGS" 2>/dev/null)
  if [ -n "$HAS_TAXI" ]; then
    jq '
      .hooks.SessionStart |= [
        .[] | .hooks |= [.[] | select(.command
          | contains("taxi-start") | not)]
      ] |
      .hooks.SessionStart |= [.[] | select(.hooks | length > 0)] |
      if .hooks.SessionStart == [] then del(.hooks.SessionStart)
        else . end |
      if .hooks == {} then del(.hooks) else . end
    ' "$SETTINGS" > "$SETTINGS.tmp" && mv "$SETTINGS.tmp" "$SETTINGS"
    echo "    โœ“ .claude/settings.json (surgically removed taxi hook)"
  else
    echo "    ยท .claude/settings.json (no taxi hook found)"
  fi
else
  echo "    ยท .claude/settings.json (no backup, not found, or jq missing)"
fi

GITIGNORE="$CLAUDE_DIR/.gitignore"
if [ -f "$BACKUP_DIR/.gitignore" ]; then
  cp "$BACKUP_DIR/.gitignore" "$GITIGNORE"
  echo "    โœ“ .claude/.gitignore (restored from backup)"
elif [ -f "$GITIGNORE" ]; then
  grep -v -E "sessions/|taxi-backup/" "$GITIGNORE" \
    | sed '/^$/d' > "$GITIGNORE.tmp"
  if [ -s "$GITIGNORE.tmp" ]; then
    mv "$GITIGNORE.tmp" "$GITIGNORE"
    echo "    โœ“ .claude/.gitignore (removed taxi lines)"
  else
    rm -f "$GITIGNORE.tmp" "$GITIGNORE"
    echo "    โœ“ .claude/.gitignore (was taxi-only, removed)"
  fi
else
  echo "    ยท .claude/.gitignore (not found)"
fi

SL_CONFIG="$CLAUDE_DIR/statusline.json"
if [ -f "$BACKUP_DIR/statusline.json" ]; then
  cp "$BACKUP_DIR/statusline.json" "$SL_CONFIG"
  echo "    โœ“ .claude/statusline.json (restored from backup)"
fi

# --- 4. Clean up backup dir ---
if [ -d "$BACKUP_DIR" ]; then
  rm -rf "$BACKUP_DIR"
  echo "    โœ“ .claude/taxi-backup/ (removed)"
fi

# --- 5. Clean up empty dirs ---
rmdir "$CLAUDE_DIR/hooks" 2>/dev/null \
  && echo "    โœ“ .claude/hooks/ (empty, removed)" || true

echo ""
echo "๐Ÿš• Taxi meter removed. No files left behind."
echo "   Your .claude/ directory and other settings are untouched."