| name | haircut-log |
| version | 0.1.0 |
| description | Log haircuts with before/after photos, dates, barber, style notes. Track haircut history over time. |
| triggers | ["haircut","barber","hair style","before after","haircut log","haircut history"] |
| metadata | {"hermes":{"tags":["lifestyle","photos","personal","grooming"],"related_skills":[]}} |
Haircut Log
Track haircuts with photos, dates, barber details, style notes, and optional ratings.
Storage
- Log file:
/opt/data/haircuts/log.json — structured JSON with schema_version and entries array
- Photos:
/opt/data/haircuts/photos/ — named {id}_{kind}.{ext} (e.g., 20260511-01_before.jpg)
Schema (v1)
{
"schema_version": 1,
"entries": [
{
"id": "20260511-01",
"date": "2026-05-11",
"created_at": "2026-05-11T10:30:00Z",
"updated_at": "2026-05-11T18:00:00Z",
"barber": "Name or shop",
"style": "Description of cut",
"tags": ["fade", "short"],
"cost": null,
"notes": "Any extra notes",
"photos": [
{"kind": "before", "path": "photos/20260511-01_before.jpg"},
{"kind": "after", "path": "photos/20260511-01_after.jpg"}
],
"rating": null
}
]
}
Fields
id — YYYYMMDD-NN (date + sequence number for same-day duplicates). Required.
date — Haircut date, YYYY-MM-DD. Required.
created_at — ISO 8601 timestamp when entry was created. Required.
updated_at — ISO 8601 timestamp of last modification. Updated on rating, photo additions, etc.
barber — Name of barber or shop. Optional.
style — Description of the haircut/style requested. Optional.
tags — Array of searchable tags: ["fade", "beard-trim", "short", "long"]. Optional.
cost — Price in dollars (number). Optional.
notes — Freeform notes. Optional.
photos — Array of {kind, path} objects. kind is "before", "after", "mid", or "other". Optional.
rating — 1-5 rating, can be added later. Optional (stored as null until rated).
Usage
Logging a New Haircut
When user mentions getting a haircut or sends a haircut photo:
- Photo processing with vision model:
- Use
vision_analyze(image_url=..., question="Describe this photo — is it a before or after haircut photo? Describe the haircut/style.") to process the image
vision_analyze auto-falls back to a vision-capable model when the current model doesn't support vision
- This lets you "see" the photo even on non-vision models
- Save the photo to
/opt/data/haircuts/photos/{id}_{kind}.{ext}
- If the image is a URL, download it:
curl -o /opt/data/haircuts/photos/{id}_{kind}.{ext} "<url>"
- If the image is a local file path, copy it:
cp <path> /opt/data/haircuts/photos/{id}_{kind}.{ext}
- Create entry with today's date and a new
id (check existing entries for same-day collisions — increment NN)
- Ask minimally — if user just sends a photo, log it. Don't interrogate about barber/style/notes unless they volunteer.
Adding a Photo Later
If user sends another photo for the same day:
- Process with
vision_analyze to identify if it's before/after/mid/other
- Find existing entry for today (or most recent entry if sent same day)
- Add photo to the entry's
photos array with appropriate kind
- Update
updated_at
Rating Later
When user says "rate my last haircut" or "4 out of 5":
- Find the most recent entry with
rating: null (or the one they're referring to)
- Set
rating, update updated_at
Updating an Entry
If user corrects barber, style, or notes:
- Find entry by date or "last one"
- Update the field(s), set
updated_at
Queries
- "Show last haircut" → Read last entry, use
vision_analyze to show/describe the photo
- "How often do I get haircuts?" → Compute average days between entries
- "Show all by Tony" → Filter entries by barber field
- "Show all fades" → Filter by tags containing "fade"
Atomic Write Rule
Always write atomically to prevent corruption:
import json, os, tempfile
fd, tmp = tempfile.mkstemp(dir="/opt/data/haircuts", suffix=".tmp")
try:
with os.fdopen(fd, "w") as f:
json.dump(data, f, indent=2)
os.replace(tmp, "/opt/data/haircuts/log.json")
except:
os.unlink(tmp)
raise
Backup Rule
Before writing, copy log.json to log.json.bak if it exists.
Setup
Create directories on first use:
mkdir -p /opt/data/haircuts/photos
If log.json doesn't exist, create it as {"schema_version": 1, "entries": []}.
Vision Setup (required for photo analysis)
vision_analyze needs an explicitly configured vision provider. provider: auto silently fails. Edit /opt/data/config.yaml:
auxiliary:
vision:
provider: openrouter
model: google/gemini-2.0-flash-001
base_url: https://openrouter.ai/api/v1
api_key: ''
timeout: 120
After editing, restart the gateway for changes to take effect:
fly apps restart hermes-agent
The config change must also be synced to GitHub (flyio/config.yaml in the Hakan repo). See references/vision-setup.md for alternatives and troubleshooting.
Pitfalls
-
EXIF leaks location — Always strip EXIF from photos before saving. Telegram re-encodes photos (usually strips EXIF), but direct file uploads may retain it. If Pillow is available: python3 -c "from PIL import Image; Image.open(f).save(f, exif=b'')". If not, note it as a known limitation.
-
Vision model for photos — vision_analyze requires a vision provider explicitly configured in config.yaml. The provider: auto setting silently fails even when API keys are present. You must set all three: provider, model, and base_url. See references/vision-setup.md for the exact config. Without this, photos can still be saved but cannot be analyzed/described.
-
Same-day collisions — Use YYYYMMDD-NN IDs so two haircuts on the same day don't overwrite each other.
-
Photo extension varies — Telegram sends WEBP, iPhone sends HEIC, screenshots are PNG. Don't hardcode .jpg; use the actual extension.
-
Atomic writes — Never write directly to log.json. Always write to a temp file then os.replace().
-
Don't ask too many questions — If user sends a photo, log it. Barber/style/notes are optional; rating can come later.
-
Date is haircut date, not log date — If user backfills ("I got a haircut last Tuesday"), use last Tuesday's date.
-
Backup before write — Copy to log.json.bak before any modification.
-
Timezone — Use the user's timezone for "today" determination. Rishi is in ET (America/New_York).