Set your rate. Get an install command. Paste it in your project.
~/my-project | ๐ 12m30s $31.25 ($150/hr)
#!/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)"
.claude/hooks/taxi-start.sh โ the timer hook.claude/taxi-config.json โ your rate.claude/statusline.json โ enables taxi module.claude/sessions/ โ timer data (gitignored).claude/settings.json โ appends SessionStart hook.claude/statusline.json โ adds "taxi" to modules.claude/.gitignore โ adds sessions/ line.claude/taxi-backup/ before any changes.
Uninstall restores them exactly.
Restores your original files from backup, removes everything taxi-meter added.
#!/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."