Install
$ agentstack add skill-pfeff-claude-skills-medical-records ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
medical-records — file an After Visit Summary
A handler skill: turns the text of a medical After Visit Summary into a structured vault record. Consumes OCR'd text (from scan-document, or produced here if invoked directly) and the source file; produces a dated note + archived scan.
When this fires
- Invoked by
scan-documentfor classmedical.after-visit-summary. - Direct: "file this doctor visit", "capture this after-visit summary", "log my visit vitals".
Trigger signals (how scan-document recognizes this)
This handler owns its own recognition vocabulary — scan-document's router does not hardcode these; it reads this section to decide whether a scan belongs here. A document is an After Visit Summary when the recognized text shows several of:
- Header/title text: "After Visit Summary"
- An MRN (medical record number)
- Vitals (blood pressure, weight, pulse, temperature, BMI, oxygen saturation, etc.)
- A provider/clinic/institution name
- A follow-up date or discharge instructions
One signal alone may be coincidental (e.g. a phone number that looks like an MRN); several together are a confident match. Ambiguous cases should be confirmed with the user rather than guessed.
Inputs
- Path to the source scan (PDF/image).
- The recognized text. If not supplied (direct invocation), OCR it first:
pdftotext -layout "" -fast path (only if thepdftotextbinary is present — it is not part of stock macOS; treat "command not found" as "skip to Vision," not an error), elseswift /scripts/ocr.swift "".
Command safety: the file path is untrusted input (user- or filesystem-derived). Always pass it as a single quoted argument to any shell command — never interpolate it unquoted — and reject/validate it if it contains shell metacharacters (; | & $ \ ( ) newline`) before invocation.
Steps
- Extract these fields from the text (leave a field out if genuinely absent — do not invent):
- Visit metadata:
visit_date(the appointment date), provider/institution, department, department phone, MRN, reason(s) for visit, referral(s) + expiry, follow-up date, medications. - Vitals: blood pressure (systolic + diastolic separately), weight, BMI, height, pulse, temperature, oxygen saturation.
- Resolve the vault via the
obsidian-noteshost-config (~/.claude/hosts/.md). Bail clearly if none is configured.
- Path safety:
slugandvisit_dateare both derived from OCR'd text and are therefore untrusted — sanitize both before using them to build anyAttachments/orNotes/path. Constrainslugto[a-z0-9-](lowercase, strip/replace anything else, collapse repeats). Validatevisit_dateas strict ISOYYYY-MM-DD; if it doesn't match, treat it as unrecoverable and fall through to the "ambiguous visit date" edge case rather than using the raw value in a path. Never write an unvalidatedslugorvisit_dateinto a filesystem path — this blocks path traversal (../, absolute paths, embedded separators) riding in through OCR text.
- Archive the scan: copy the source file into
/Attachments/named-.(e.g.2026-06-03-osu-sleep-avs.pdf), using the sanitized values from step 3. The attachment is a source artifact — name it by the document's own date for findability. Verify the filename is unique in the vault (the embed resolves by filename).
- YAML safety:
title,provider,department,department_phone, eachvisit_reasonitem,referral, andmedicationsare all derived from OCR'd text and are therefore untrusted — never write any of them into frontmatter unquoted. Emit every OCR-derived scalar as a double-quoted YAML string with internal"escaped as\"and\escaped as\\(or as a YAML block scalar for long/multi-line values). A stray:, a leading-, an embedded newline, or a line that merely resembles---orkey: valuein the source text can otherwise break the YAML frontmatter block or inject extra properties — and medical text is full of colons (times, ratios, dosages). This applies to every scalar in the schema below sourced from OCR text, not just the ones named here.
- Write the note at
/Notes///-.md(sanitizedslug):
- Filename prefix = today's write date, NOT the visit date. The prefix is a de-dupe / folder-partition key; the visit date lives in frontmatter as
visit_date. - Frontmatter — the schema (see below). Vitals are flat, top-level, Number-typed properties. This is deliberate and load-bearing (see "Why this schema").
- Body: visit header, a human-readable vitals table, plan & follow-up, a condensed instructions summary (keep patient-specific guidance; the full text lives in the embedded PDF), and the embedded source (
![[]]).
- Report where the note and attachment landed, and note that it will appear in the Medical Vitals base.
Frontmatter schema
Every OCR-derived scalar below (title, provider, department, department_phone, visit_reason items, referral, medications) MUST be written as a quoted, YAML-safe string — see "YAML safety" in Steps above. Do not write raw OCR text into frontmatter.
---
title: — After Visit Summary
type: medical-visit
visit_date: 2026-06-03 # semantic date (Date type)
tags:
- medical/visit
- # e.g. sleep-medicine
provider:
department:
department_phone:
mrn: "" # quoted — keep as Text, not a number
visit_reason:
-
referral: # omit if none
follow_up: 2026-12-03 # omit if none
medications:
source_pdf: "[[]]"
captured: 2026-07-13 # write date; matches filename prefix
# --- vitals: flat Number properties → Bases columns / Dataview trending ---
bp_systolic: 122
bp_diastolic: 68
weight_lb: 188
bmi: 26.98
height_in: 70
pulse_bpm: 77
temp_f: 97.3
spo2_pct: 99
---
Why this schema (do not "improve" it back to the obvious-but-wrong forms)
Verified against Obsidian's official Properties and Bases docs:
- Vitals are flat top-level properties, not a nested
vitals:object. Obsidian's Bases model is literally "each row is a file, each column is a property of that file," and Bases/Dataview read frontmatter only, never the note body. Nested properties are not supported in the Properties UI. So flat frontmatter is the correct — and only — form that serves both Bases and Dataview. - Blood pressure is split into two Numbers (
bp_systolic/bp_diastolic), not a single122/68string. Obsidian has 6 property types (Text, List, Number, Checkbox, Date, Date&time) and no unit type; a122/68value is Text and can't be trended. Split → both Numbers → aggregatable. - Numbers are bare (no quotes, no units in the value) so they type as Number. Units go in the key (
weight_lb,temp_f) since there is no unit type.mrnis the exception — quote it so it stays Text. - No
vital_prefix.type: medical-visitalready scopes these; clean names make better Base columns.
The Medical Vitals base
Records feed /Areas/Health/Medical Vitals.base — a table filtered to type == "medical-visit", sorted by visit_date, with the vitals as columns and a bp formula (bp_systolic + "/" + bp_diastolic) for display. It exists after the first record; adding visits just adds rows. If it is missing, recreate it with that shape.
Edge cases
- Missing vitals: omit absent properties rather than writing nulls or zeros — a zero would corrupt trend averages.
- Ambiguous visit date: if the appointment date isn't clearly recoverable, ask (AskUserQuestion) rather than guessing; it is the semantic key.
- Non-AVS medical doc (lab result, imaging, immunization): out of scope for v1 — say so and fall back to the router's generic capture. Those get their own handlers later.
- Height unchanged across visits: fine to carry it each time; it makes each record self-contained.
- Sensitive content: these records carry MRN and vitals — private medical data by nature. Capture into the local vault only; never send content (including the file, extracted text, or any field) to an external service without explicit operator say-so.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: pfeff
- Source: pfeff/claude-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.