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.

A dusty blue mailbox with an orange envelope stamped OVERDUE stuck in the slot, a desk calendar showing 16 and a phone lying face down

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

Left: the sweep notices a letter and pings me. Right: the weekly question, answered in five lines.

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

A tidy desk with three open folders glowing softly, linked by light trails to a small orange spark that answers a question-mark bubble

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.

claude ~

when can I give notice on the apartment, and how much notice do I need?

read Identity/Home.mdread Tracks/LeaseDeposit.md

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?

One question, two file reads, a dated answer.

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.

claude ~

shop support called back. refund approved, ticket 48213, money by friday

edit Tracks/ShopRefund.mdappend Journal/2026-10-19.mdpa task "Check refund arrived" 2026-10-24

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.

One sentence from me, three writes from the agent.

Ask where a matter stands

Weeks later I don’t scroll through mail. The track lists every step with its date.

claude ~

what's going on with the passport renewal?

read Tracks/PassportRenewal.md

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.

A track holds a matter's whole history, so 'where are we?' costs one read.

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.

A robotic arm pulls an index card from a filing cabinet to fill the next field of a form on a clipboard, while a SUBMIT stamp sits untouched beside its ink pad

πŸ”’ city-services.example/registration

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.

Claude in Chrome fills a real form from Identity/. It stops at the one field it doesn't know and never presses Submit. (All data here is made up.)

Get a weekly recap

On Sunday evening, /recap 7 reads closed tasks, past events and the week’s journal files:

An open weekly planner under a desk lamp’s warm evening light, almost every entry ticked and one circled in orange as stuck

claude ~ /recap 7

/recap 7

pa recent 7read Journal/2026-10-13…19.md

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?

Built from the journal and Google Tasks. I wrote none of it down myself.

The architecture

Personal assistant architecture: inputs, Claude Code, Google, Chrome, Obsidian vault and hooksINPUTSBRAINMEMORY & SCHEDULEYou, in a chat"remind me…", "what's on today?"Gmailletters with dates & deadlineslaunchdheadless sweep every 3 hoursClaude Codeassistant skillrules: what matters, what is noisepa CLI β†’ GoogleCalendar events Β· TasksClaude in Chromefills forms from Identity/~/Vault Β· ObsidianIdentity/ β€” factsTracks/ β€” ongoingJournal/ β€” per dayIndex.md β€” mapHooksPostToolUse β†’ vault-sync.pystamps updated:, rebuilds IndexStop β†’ "did you write it down?"
One brain, three inputs, two kinds of storage, and a browser for forms. Hooks keep the vault consistent without asking the model to.

There are six moving parts:

  1. An Obsidian vault (~/Vault): plain Markdown in iCloud, readable on my phone.
  2. A skill (~/.claude/skills/assistant/SKILL.md): what the assistant is, what matters, what is noise, and where each kind of information goes.
  3. pa: a ~300-line, stdlib-only Python CLI over Google Calendar and Google Tasks.
  4. Two hooks: one keeps the vault consistent, the other makes the session write things down.
  5. A launchd job: a headless Claude run every three hours that triages mail.
  6. 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

A filing cabinet with three drawers marked by an ID card, a status folder and a calendar; one sheet on top is linked by a thread to a single folder

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.

~/Vault
  • πŸ“„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 whole vault. Folders are the schema; frontmatter is the state.

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

Every new piece of information answers one of four questions. Important things often go to two bins at once.
  • 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:
Tracks/LeaseDeposit.md
---
type: track
status: active # active | watch | closed
updated: 2026-10-12 # set by a hook, never by hand
note: landlord promised the refund by 31.10, chase on 01.11
tags: [housing, money]
---
# Lease deposit refund
- 2026-10-02 β€” inspection done, two minor items noted
- 2026-10-12 β€” landlord confirmed the deposit minus cleaning
  • Assistant/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

A sieve catching three bright envelopes while the rest of the mail falls through as grey confetti

The skill is the assistant’s job description. The blunt rules changed its behaviour most:

InsurerAction required: proof of address→ task, due 24 Oct
Shop newsletter48h only: 30% off everythingnoise
GitHub[ci] build passed on mainnoise
LandlordDeposit inspection results→ track updated
BankYour monthly statement is readynoise
ClinicAppointment confirmed: Wed 09:30β†’ calendar event
Delivery serviceYour order has shippednoise
Seven letters in, three actions out. The noise list decides what never reaches me.
~/.claude/skills/assistant/SKILL.md (excerpt)
## 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

A terminal window with a robotic arm pinning an orange sticky note onto a wall calendar

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
$ ▍
Mon
Tue
Wed09:30 Dentist
Thu18:00 Viewing
FriProof of address
Sat
SunRenew passport
Same data, two faces: the text the model reads and the calendar I look at.
Terminal window
pa agenda 7 # events + tasks for a week
pa event "Dentist" "2026-10-22 09:30" 45 # create an event
pa task "Renew passport" 2026-10-30 "photo, form" # task with a deadline
pa done "passport" # close by substring
pa recent 7 # what happened, for weekly recaps

cancel 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

A violet hook catches a note falling off a desk just above an open journal

I don’t want to depend on the model remembering some things.

PostToolUse Β· every write

  1. 1Claude edits Tracks/LeaseDeposit.md
  2. 2vault-sync.py stamps updated: today
  3. 3Index.md table rebuilt from every track

Stop Β· end of session

  1. 1Session is about to end
  2. 2vault-reminder.sh: nothing written today?
  3. 3Claude adds a journal entry, then stops
Two hooks, two guarantees: the index never drifts, and a session never ends without writing things down.

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.

Same vault, two weeks after the refund arrived. Instructions alone produced the left side; the hook produces the right.
~/.claude/hooks/vault-sync.py (core)
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>.md
If 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:

claude ~

ok, so I call the landlord tomorrow and ask for the radiator fix before the 1st. thanks, bye

βš“ Stop hook

The vault was not updated today. If something personal came up in this session, decide where it goes and write it down…

edit Tracks/ApartmentRepairs.mdappend Journal/2026-10-19.mdpa task "Call landlord about radiator" 2026-10-20

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!

I never asked it to save anything. The Stop hook did.

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 lighthouse beam sweeps over a row of sleeping mailboxes at night and lands on the only one with a letter in it

A launchd agent runs claude -p headless every three hours:

NOTHING01:47
NOTHING04:47
πŸ”” Dentist confirmed β†’ calendar07:47
NOTHING10:47
NOTHING13:47
πŸ”” Insurer letter β†’ task, due Fri16:47
NOTHING19:47
NOTHING22:47
Eight runs a day. Six print NOTHING and stay silent; two have something to say.
~/bin/AssistantSweep.sh (shape)
# 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 grep them, 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:

Or start a Claude Code session in your home directory and paste:

prompt.md
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.