Claude Code as a Life-Admin Assistant: A Markdown Vault, Two Hooks and a Mail Sweep
A personal assistant built from Claude Code, a folder of Markdown, a small calendar CLI and two hooks. It turns letters into dated tasks, keeps a file for every ongoing matter and pings you only when something happened. The setup prompt is at the end.

None of my life admin is hard. There is just a lot of it: forty small things, each with its own deadline, inbox and login, and the only place they meet is my head. An insurer gives me 14 days to send a document. I read the letter on my phone, think “later”, and find it again on day 16.

I spend my working day in Claude Code, so I gave life admin the setup I use for code: plain text files, a skill with rules, a small CLI for side effects, and hooks for anything that must happen every time. Two things changed first:
- A letter with a deadline arrives. I don’t open it. Within three hours my task list has “Send proof of address to the insurer, due 24 Oct”, and the next morning summary starts with it.
- I ask “what’s on this week?” I get five lines back: two appointments, a task due Thursday, one matter waiting on someone else, and “the rest is noise”.
Assistantnow
New task: send proof of address to the insurer, due Fri 24 Oct. Added to tomorrow's summary.
Assistant3h ago
Dentist confirmed Wed 22 Oct 09:30, in calendar.
what's on this week?
📅 Wed 09:30 dentist · Thu 18:00 viewing
⏰ Fri proof of address to insurer
⏳ shop refund: waiting on them since 07.10
⚠️ overdue: nothing
the rest is noise
I deploy nothing. The whole thing is a folder of Markdown, one skill, a ~300-line CLI and two hooks. Launchd and Keychain are macOS-specific and easy to swap.
What using it looks like

Most days I use the assistant like a colleague who has read every letter I’ve received: I ask, it looks things up and writes down whatever changed. The chips above each reply show which files and commands it touched.
Ask anything, any time
I don’t remember my lease terms or my policy number. The vault does, so I ask from any terminal.
when can I give notice on the apartment, and how much notice do I need?
Your lease started 1 Mar 2025 with a 12-month minimum, so you can give notice now. Notice period is one calendar month, in writing. Give it by 31 Oct and you move out 30 Nov. Want me to draft the letter and add a task?
Tell it what happened
After a call I tell the assistant in one sentence. It puts the new state in the track, the details in the journal and anything dated in the calendar.
shop support called back. refund approved, ticket 48213, money by friday
Updated the refund track: approved, ticket 48213, expected by Fri 24 Oct. Logged the call in today's journal and set a task for Friday to check the bank. If it hasn't arrived, the reminder draft is ready.
Ask where a matter stands
Weeks later I don’t scroll through mail. The track lists every step with its date.
what's going on with the passport renewal?
Waiting on the embassy. Applied 2 Oct, paid the fee the same day, biometrics done 9 Oct. They quoted 4–6 weeks, so the earliest is 6 Nov. I set a task for 13 Nov to chase if nothing arrives. Nothing is needed from you now.
Let it fill forms
Registration forms and insurance claims ask for the same twenty fields. With the Claude in
Chrome extension the assistant reads the page and fills it from Identity/. It asks about
anything the vault lacks and leaves the Submit button to me.

Register your address
Submit✳️ Claude
Filled 6 of 7 fields from your vault.
Previous address isn't in Identity/. Type it here and I'll fill it and save it to Home.md, or I'll leave it empty.
I haven't pressed Submit.
Get a weekly recap
On Sunday evening, /recap 7 reads closed tasks, past events and the week’s journal files:

/recap 7
Done: dentist, proof of address sent to the insurer, parking permit renewed.
In progress: shop refund (approved, due Fri), passport (embassy, earliest 6 Nov).
Overdue: nothing.
Stuck: the landlord hasn't answered about the radiator for 9 days. Want a reminder draft?
The architecture
There are six moving parts:
- An Obsidian vault (
~/Vault): plain Markdown in iCloud, readable on my phone. - A skill (
~/.claude/skills/assistant/SKILL.md): what the assistant is, what matters, what is noise, and where each kind of information goes. pa: a ~300-line, stdlib-only Python CLI over Google Calendar and Google Tasks.- Two hooks: one keeps the vault consistent, the other makes the session write things down.
- A launchd job: a headless Claude run every three hours that triages mail.
- Claude in Chrome: the browser extension that fills web forms from
Identity/.
I run no server, database or vector store. The model reads files when it needs them.
The vault: one fact, one file

The most useful rule in the system sits at the top of Index.md:
A fact lives in one file. Everything else links to it.
- 📄
Index.mdmap + live tracks table - 📁
Identity/facts - 📄
Person.md · Home.md · Documents.md · Bank.md - 📁
Tracks/one file per matter - 🔴
LeaseDeposit.mdactive - 🟡
PassportRenewal.mdwatch - ✅
ShopRefund.mdclosed, kept - 📁
Assistant/Journal/append-only - 📄
2026-10-18.md · 2026-10-19.md - 📁
Documents/ · Health/ · Finance/scans & PDFs
The vault holds three kinds of content:
Is it a stable fact?
Identity/
new address, policy number, bank details
edit the one file that owns it
Is it an ongoing matter?
Tracks/
a dispute, an application, a claim
append progress, update status: and note:
Did it just happen?
Journal/YYYY-MM-DD
a call, a decision, an amount, a case number
append one entry, never rewrite
Does it have a date?
Calendar / Tasks
appointment, deadline, follow-up
pa event / pa task, absolute dates only
Identity/: name, address, documents, bank details, work. It rarely changes. When Claude fills a form or drafts a letter, it reads from here instead of asking me.Tracks/: one file per ongoing matter, with frontmatter the agent maintains:
---type: trackstatus: active # active | watch | closedupdated: 2026-10-12 # set by a hook, never by handnote: landlord promised the refund by 31.10, chase on 01.11tags: [housing, money]---# Lease deposit refund
- 2026-10-02 — inspection done, two minor items noted- 2026-10-12 — landlord confirmed the deposit minus cleaningAssistant/Journal/: one file per day with calls, decisions, amounts and case numbers. The journal answers what happened today; a track answers where this matter stands. Important things go to both.
When a matter is done, the agent sets status: closed and deletes nothing. A year later,
when the same problem comes back, the closed track has the names, dates and numbers.
The skill: an opinionated job description

The skill is the assistant’s job description. The blunt rules changed its behaviour most:
## How to behave- **Create, don't ask.** An agreement with a date and time → create the event. "Need to do X by Friday" → create a task with a deadline. Ask only if the date is unclear or it conflicts with an existing event.- **A task without a date is a bad task.** If no deadline was named, propose one and set it.- **Conflicts.** Before `pa event`, check `pa agenda` for that day.
## What matters, what is noise**Important:** insurance, housing, tax, official documents, doctors, disputes with shops.Always show it and create a task with a deadline.**Noise:** marketing, receipts, CI notifications, service alerts.Say "the rest is noise" in one line and move on.The noise list matters as much as the signal list. Without it the morning summary runs to thirty lines, and I skim a summary that long instead of reading it.
pa: a boring CLI for side effects

I don’t want the model clicking around a calendar UI. It gets a CLI with the verbs it needs:
$ pa agenda 7 Wed 22.10 09:30 Dentist (45m) Thu 23.10 18:00 Viewing, Oak St 12 Fri 24.10 ☐ Proof of address to insurer Sun 26.10 ☐ Renew passport $ ▍
pa agenda 7 # events + tasks for a weekpa event "Dentist" "2026-10-22 09:30" 45 # create an eventpa task "Renew passport" 2026-10-30 "photo, form" # task with a deadlinepa done "passport" # close by substringpa recent 7 # what happened, for weekly recapscancel and auth round out the set. Two design decisions:
- Absolute dates only. The CLI rejects “tomorrow”. The model converts relative dates itself from today’s date in the session context, and my off-by-one-day bugs went away.
- Standard library only. OAuth runs through a loopback server; API calls use plain
urllib. A Python update has no dependencies to break.
Hooks: what must happen every time

I don’t want to depend on the model remembering some things.
PostToolUse · every write
- 1Claude edits Tracks/LeaseDeposit.md
- 2vault-sync.py stamps updated: today
- 3Index.md table rebuilt from every track
Stop · end of session
- 1Session is about to end
- 2vault-reminder.sh: nothing written today?
- 3Claude adds a journal entry, then stops
PostToolUse → vault-sync.py. After every Write or Edit inside the vault, the hook
stamps updated: in the file’s frontmatter. If the file is a track, it rebuilds the
“What we’re tracking” table in Index.md from every track’s status: and note:.
Nobody edits that table by hand, so it can’t drift from the tracks.
Before this hook, the model maintained the table. The instruction was clear, and the model still skipped it now and then: it closed a track and forgot the overview. When that happened, the morning summary repeated a stale state with full confidence:
Without the hook
Index.md · What we're tracking
🔴 ShopRefund: waiting for the shop, chase on 03.10
Tracks/ShopRefund.md
status: closed · refund received 05.10
The overview lies. Every summary built on it lies too.
With vault-sync.py
Index.md · What we're tracking
✅ ShopRefund: refund received 05.10
Tracks/ShopRefund.md
status: closed · refund received 05.10
The table is rebuilt from the tracks on every write. It can't drift.
def rebuild_index(): rows = [] for p in sorted((VAULT / "Tracks").glob("*.md")): fm = frontmatter(p.read_text()) mark = MARK.get(fm.get("status", "active"), "🔴") rows.append((RANK[mark], fm.get("updated", ""), p.stem, fm.get("note", ""))) rows.sort(key=lambda r: (r[0], -int(r[1].replace("-", "") or 0))) table = ["| Track | Status |", "|---|---|"] + [ f"| [[{stem}]] | {mark} {note} |" for _, _, stem, note in rows] # replace the section between "## What we're tracking" and the next "## "The hook runs on every file write, so it has to be boring and correct. A --selftest
flag runs a few assertions against a temp directory.
Stop → vault-reminder.sh. When an assistant session ends and nothing in the vault
changed today, the hook blocks the stop once and asks:
The vault was not updated today. If something personal came up in this session,decide where it goes and write it down:• a new or changed fact → Identity/• an ongoing matter moved → its file in Tracks/, update status: and note:• decisions, amounts, case numbers → Journal/<today>.mdIf the session was purely technical, say so in one line and finish.Here is what that looks like at the end of an ordinary chat, where I only asked for advice and was about to close the terminal:
ok, so I call the landlord tomorrow and ask for the radiator fix before the 1st. thanks, bye
The vault was not updated today. If something personal came up in this session, decide where it goes and write it down…
Saved before closing: the radiator issue is now a track, today's plan is in the journal, and tomorrow has a task to call the landlord. Bye!
A good share of what ends up in the vault comes from this nudge. The model writes things down well once asked; left alone, it forgets.
The sweep: mail triage while I’m away

A launchd agent runs claude -p headless every three hours:
# Mail window = time since the last successful run (+1h overlap), clamped to 4..48h.# If the laptop slept half a day, the window grows and nothing is lost.OUT=$(claude -p "$PROMPT" \ --allowedTools "mcp__gmail Bash Read Write Edit Glob Grep" \ --model sonnet)
# Notify only when there is something to say.# The text goes in as an argument, so quotes in it can't break the AppleScript.notify() { osascript -e 'on run argv' -e 'display notification (item 1 of argv) with title "Assistant"' -e 'end run' "$1"; }[ "${OUT#NOTHING}" = "$OUT" ] && notify "$OUT"The prompt tells it to read new mail, create events and tasks (checking pa agenda
first to avoid duplicates), append to the journal, move tracks forward and log spending
to a CSV. If nothing happened, the output must be exactly one word: NOTHING. On a quiet
day I see no notification at all, even though the sweep runs eight times.
The sweep never sends, deletes or archives anything. It saves replies as Gmail drafts, and I read and send them myself.
What I’d tell you before you build one
Four rules I’d keep if I started over:
- Use plain files. The agent can
grepthem, I can read them on my phone, and iCloud keeps file versions. - Put mechanical work in hooks. A clear instruction still gets skipped sometimes; my index table drifted until a hook took it over. If something must happen every time, write code for it.
- Write the noise list first. An assistant that reports everything is one more inbox.
- Let the agent draft; send it yourself. Nothing leaves my accounts without me reading it.
Build your own: paste this into Claude Code
All the code from this post is on GitHub, ready to copy:
Claude Code as a life-admin assistant: Markdown vault, skill, calendar CLI, hooks and a mail sweep.
git clone https://github.com/vasilinoff/claude-life-adminOr start a Claude Code session in your home directory and paste:
Set up a personal assistant for me, step by step. Ask before anything irreversible.
1. Create an Obsidian vault at ~/Vault with: - Index.md: a map of the vault and a "What we're tracking" section; - Identity/: one file per topic (Person, Home, Documents, Bank, Work); - Tracks/: one file per ongoing matter, frontmatter: type, status (active|watch|closed), updated, note; - Assistant/Journal/: one file per day, YYYY-MM-DD.md, append-only. Rule: a fact lives in one file, everything else links to it. No secrets in the vault.
2. Write a skill at ~/.claude/skills/assistant/SKILL.md: when to use it (non-coding life admin), what is important to me (ask me), what is noise, and where each kind of information goes. Behaviour: create events and tasks instead of asking; give every task a deadline; check for calendar conflicts first; never send email, only draft.
3. Write a stdlib-only Python CLI `pa` next to the skill with the verbs agenda, event, cancel, task, done, recent and auth, over Google Calendar and Google Tasks with a Desktop OAuth client. Accept absolute dates only.
4. Add hooks to ~/.claude/settings.json: - PostToolUse on Write|Edit: stamp `updated:` in vault files and rebuild the tracks table in Index.md from each track's status and note. Include a --selftest. - Stop: if no vault file changed today, block once and ask where today's facts should go.
5. Optional: a launchd job that runs `claude -p` every 3 hours to triage new mail into events, tasks, journal entries and track updates. It prints NOTHING when nothing happened and never sends, deletes or archives mail.Then use it for a week. Expect the first version of each rule to be wrong somewhere; fixing the rules is most of the work.