obsidian — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited obsidian (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
When the user sends a URL or asks to save/archive something: archive it immediately, do not summarize. Your job is that of a librarian: receive → process → file. Not a chatbot to give impressions.
Do NOT respond with a summary of the content unless the user explicitly asks for one. Archive first, then say only "Salvato." (or the error, if any).
<profile-dir>/vault/
### [Title]
- **URL**: https://...
- **Type**: `github` | `x-post` | `article` | `tool` | `video` | `paper` | `other`
- **Tags**: #tag1 #tag2 #tag3
- **Added**: YYYY-MM-DD
- **Summary**: What is this? Why does it matter? What do you do with it?
---
[next entry]--- between every entry. No --- = merged entry in dashboard. Parse splits on \n---\n.
Use `save_entry.py` — atomic read+patch, never overwrites INDEX.md.
python3 <profile-dir>/skills/note-taking/obsidian/scripts/save_entry.py \
--url "https://..." \
--title "Entry Title" \
--type "article" \
--tags "ai dev-tools" \
--added "2026-06-04" \
--summary "What it is and why it matters."The script:
--- separatorvault/YYYY-MM-DD.md⚠️ Prepend bug (fixed): Old versions ofsave_entry.pyinserted new entries BETWEEN the header and the first---. When two entries from the same day were saved consecutively, they ended up in the same chunk. The dashboard parser usesre.findall(r'\*\*URL\*\*', chunk)[0]— only the first URL per chunk was read. Symptoms: entry appears in INDEX.md but not in dashboard (or dashboard shows fewer entries than vault count). Fix was to changelines[:sep_idx]→lines[:sep_idx+1]so insertion happens AFTER the separator, not before. Runrebuild_index.pyto fix retroactively affected vaults.
Validate after save is automatic via the script. Manual validate (if needed):
cd <profile-dir>/dashboard && python3 validate.pyManual workflow (only if script unavailable): fetch → append to daily → read INDEX first, then patch to prepend. Never use write_file on INDEX.md.
web_extract and browser_navigate may return login walls on X. If camofox is installed and the content is important, use its local REST API:
CAMOFOX_USER_ID="${CAMOFOX_USER_ID:-link-curator}"
TAB=$(curl -s -X POST http://localhost:9377/tabs \
-H "Content-Type: application/json" \
-d "{\"userId\":\"$CAMOFOX_USER_ID\",\"sessionKey\":\"link-curator\",\"url\":\"https://x.com/USERNAME/status/POST_ID\"}")
TAB_ID=$(echo "$TAB" | python3 -c "import sys,json; print(json.load(sys.stdin)['tabId'])")
sleep 3
curl -s "http://localhost:9377/tabs/$TAB_ID/snapshot?userId=$CAMOFOX_USER_ID" > /tmp/snap.json
python3 -c "import sys,json; d=json.load(open('/tmp/snap.json')); print(d.get('snapshot','')[:6000])"
curl -s -X DELETE "http://localhost:9377/tabs/$TAB_ID?userId=$CAMOFOX_USER_ID"Content is in .snapshot, NOT .accessibilityTree.content. jq is not available — always use python3 for JSON parsing.
If camofox fails: web_search fallback, then save URL + [content unavailable] in Summary.
Quick probe before long fetch — when the URL is an X post and you need content for the Summary:
curl -s --max-time 8 "https://x.com/USER/status/ID" — if it returns HTML login page, X is blockingweb_search or save with [content unavailable]When the user says "this link is about X" or provides the summary framing directly, trust it and stop digging. Do not spend extra tool calls trying to extract more context from the page — the user already told you what matters. Supplement with whatever minimal snapshot data is readily available, then save. Examples:
"é un video che spiega che codex può lanciare, pinnare e gestire worktrees" → use that framing, add minimal post metadata (author, engagement) from snapshot, done."save this, it's about local LLMs" → accept the user's framing, don't try to fetch deeper content.Only fall back to [content unavailable] if camofox itself fails AND no user context was provided.
# By content
grep -ri "keyword" <profile-dir>/vault/ --include="*.md"
# By date
ls <profile-dir>/vault/2026-05-*.mdThe validate script lives in the dashboard directory, NOT in the profile vault. In a standard install the dashboard is at ~/.hermes/profiles/<profile>/dashboard/ and the vault is at ~/.hermes/profiles/<profile>/vault/.
Always check the actual path before running:
# Find validate.py
find ~ -name "validate.py" 2>/dev/null | grep -i dashboardNever hardcode a path you haven't verified. The cd must target the dashboard directory, not the vault.
Keep tags consistent across the vault. When saving new entries, follow these rules:
#open-source — never use #open alone#dev-tools — for developer tooling entries (CLI, agents, utility scripts). Use #dev-tools consistently, not #tooling or #tools (except for entries that are genuinely about design/general tools)#local-ai — for local inference, Apple Silicon, on-device ML#local and #local-ai → always use #local-aiDo NOT use **Status**: 'unread' or any other status markers. The vault stores processed entries only. For items to revisit later, use a bookmarking tool outside the vault (read-later app, pin board, etc.).
## Dashboard (port 8090) — separate process
The link-curator dashboard runs at `http://localhost:8090`. It is a standalone FastAPI app, not part of the official Hermes dashboard (`hermes dashboard`, default port 9119). The two are independent and can run side-by-side. See the `link-curator-dashboard` skill for full maintenance and restart procedures.
## Architecture rules (critical — never violate)
**Entry blocks do NOT contain `---`. The separator is added at write time.**
`build_entry_block()` returns text ending in `\n` (no `---`). Both `append_to_daily()` and `prepend_to_index()` add `\n---\n` or `\n---` as a separator when writing. If you put `---` inside `build_entry_block`, you get double `---` on prepend, which corrupts the dashboard parse.
**Never use `write_file` on INDEX.md.** The only safe operations are `read_file` first, then `patch`. Even for "quick edits" — read first, patch, never overwrite. The script enforces this; manual workflow must follow it too.
**INDEX.md headers differ by profile.** The `prepend_to_index` function finds the first `---` separator regardless of what header text precedes it:
- Default: `# Index` followed by `---`
- Custom profile headers are also fine as long as the first separator is `---`
Do NOT look for a specific header string — find the first `---` and insert after it.
**Validate after every save.** Run `validate.py` from the dashboard directory, not the vault.
## validate.py — common fixes
**Empty entry error ("Missing or malformed ### title line")**:
Usually caused by a double `---` separator creating an empty chunk. Fix: remove the duplicate `---` between two consecutive entries in INDEX.md, leaving only one.
**Title uses em-dash (—) instead of hyphen-minus (-)**:
The parser accepts both in titles, but the `validate.py` script warns on em-dashes. More importantly, some edge cases in the title regex (`^###\s+[^\n—]+?\s+—\s+`) can fail to parse a title that has an em-dash mid-string. **Always use hyphen-minus (`-`) as the title separator in vault entries.** Example: `### Claude Code + Screen Recording Workflow - UI Bug Fixing` not `—`.
**Title validation fails for `# Index` header**:
The title check `r'^###\s+\S'` rejects `# Heading` (single `#`). Chunk 5 (`# Index`) was a valid non-entry header — the URL filter already correctly skips non-entry blocks. Working title regex: `r'^#{1,3}\s+\S'` accepts h1–h3. The URL filter is the primary guard; the title regex is secondary.
---
## INDEX.md Health Check — when to run + manual verification
Run `validate.py` and do a manual chunk split when:
- Entries saved via `save_entry.py` appear in INDEX.md but not in the dashboard
- Dashboard shows fewer entries than vault count suggests
- You made manual edits to INDEX.md (patch, rebuild, etc.)
### validate.py output thresholds
| Result | Meaning |
|--------|---------|
| `Has errors: 0` | Vault is parse-safe. Dashboard will show all entries. |
| `Has errors: >0` | Structural corruption — entries may be missing from dashboard |
| `Fully valid: N` | Entries that pass all field checks |
| `Has warnings: M` | Non-fatal (em-dashes in titles, missing optional fields) — dashboard still works |
**Always run validate.py after any INDEX.md edit.** It's the only way to catch double-`---` chunks and merged entries before the user sees wrong counts.
### Manual chunk analysis (Python one-liner for ad-hoc checks)
content = open('<profile-dir>/vault/INDEX.md').read() chunks = content.split('\n---\n') for i, chunk in enumerate(chunks): titles = [l for l in chunk.split('\n') if l.strip().startswith('###')] if len(titles) > 1: print(f"⚠️ Chunk {i}: MERGED {len(titles)} entries") if '\n---\n' in chunk: print(f"⚠️ Chunk {i}: double separator") print(f"✅ {len(chunks)} chunks total, {sum(1 for c in chunks if 'URL' in c)} with URLs")
This catches the prepend bug (two entries in same chunk) and orphaned `---` separators (empty chunks) faster than re-reading by eye.
---
## DISASTER RECOVERY
### INDEX.md missing or overwritten
**Root cause**: `write_file` on INDEX.md without reading first wipes all entries. If only recent dates appear in dashboard (e.g. June only, May gone), INDEX.md was overwritten.
**Fix** — run the rebuild script:cd <profile-dir>/skills/note-taking/obsidian/scripts python3 rebuild_index.py
**Prevention**: ALWAYS use `save_entry.py` for new saves. Never `write_file` directly on INDEX.md.grep -c '^### ' <profile-dir>/vault/INDEX.md
### Reconciliation (INDEX vs daily notes out of sync)
If counts don't match: extract missing entries from INDEX and patch into the daily file.
### Common failure mode
`write_file` on INDEX.md without reading first = overwrite destroys all entries. Always `read_file` first, then `patch`.
## Related skills
- `camofox` — for browser-session fetching on sites that block simple extraction~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.