obsidian-vault-add — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited obsidian-vault-add (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.
Add a new resource (book, podcast, TV show, movie, person, country, or city) to the vault: create the note, download a photo/cover, update today's daily note (media only), and re-index.
Arguments: <type> <title> [extra info]
type (required): book, podcast, show (or tv), movie, person, country (or land), city (or stadt)title (required): the resource titleextra info (optional): author, host, episode number, season/episode, country name (for cities), etc.Examples:
/obsidian-vault-add book "Atlas Shrugged"/obsidian-vault-add podcast "Aethervox Ehrenfeld"/obsidian-vault-add show "Silicon Valley" 1.1/obsidian-vault-add movie "Dune"/obsidian-vault-add person "Thomas Sowell"/obsidian-vault-add country "Japan"/obsidian-vault-add city "Kyoto"If type or title is missing, ask the user before proceeding.
Search for the title to gather:
Book: title, subtitle, authors, published year, genres, pages, ISBN, short description (German preferred) Podcast: full name, host(s), language, genres, start year, description (German preferred) TV show: full name, actors, genres, network, seasons, episodes, IMDb rating, description (German preferred) Movie: full name, director(s), actors, genres, year, IMDb rating, description (German preferred) Person: full name, aliases, nationality, birthday (and death if applicable), domain/occupation, Wikipedia URL, notable works (books/articles/videos), short German biography (2–4 sentences) Country: official name, capital, continent, official language(s), currency, aliases (native name, abbreviations), short description (German preferred) City: full name, country, region/state, language(s), aliases (native name), short description (German preferred)
obsidian read file="<title>" 2>/dev/null | grep -Ev "^(20[0-9]{2}-|Your Obsidian)"If the note already exists, inform the user and stop (unless they want to update it).
| Type | Path | Template |
|---|---|---|
| book | 03 Resources/Bücher/<title>.md | Book Template |
| podcast | 03 Resources/Podcasts/<title>.md | Podcast Template |
| show | 03 Resources/Serien/<title>.md | TV Show Template |
| movie | 03 Resources/Filme/<title>.md | Movie Template |
| person | 03 Resources/Personen/<title>.md | Person Template |
| country | 03 Resources/Länder/<title>.md | Country |
| city | 03 Resources/Städte/<title>.md | City |
obsidian create path="<path>" template="<Template>" 2>/dev/null | grep -Ev "^(20[0-9]{2}-|Your Obsidian)"Edit the note file directly (Read then Edit) — do NOT use obsidian property:set for list fields as it does not reliably handle arrays. Set all fields in one Edit call by replacing the entire frontmatter block.
IMPORTANT — preserve existing data: When updating an existing note, never discard data already present. Merge new metadata with what's there:
aliases and append any new ones not already listedtags (only remove BOAT if present)Book frontmatter fields: aliases, tags: [Buch], photo, urls, published (YYYY-MM-DD), authors (list), genres (list), rating, pages
Podcast frontmatter fields: aliases, tags: [Podcast], photo, urls, hosts (list), guests, genres (list), language, rating, status, started, ended, episodes
TV show frontmatter fields: aliases, tags: [Serie], photo, urls, actors (list), genres (list), rating, imdb_rating, network, seasons, episodes
Movie frontmatter fields: aliases, tags: [Film], photo, urls, directors (list), actors (list), genres (list), rating, imdb_rating, year
Country frontmatter fields: aliases, tags: [Land], photo, urls, capital, continent, language (list), currency, visited, rating, cities (list of wikilinks to known city notes)
Person frontmatter fields: aliases (other names, pen names, maiden names), tags (Mann/Frau, nationality country, domain tags like Schriftsteller/Philosoph/etc.), photo, urls, related, birthday (YYYY-MM-DD), death (YYYY-MM-DD or empty), location, quotes (list of wikilinks), books (list of wikilinks to vault book notes), articles, podcasts, videos
City frontmatter fields: aliases, tags: [Stadt], photo, urls, country (wikilink), region, language (list), visited, rating
Remove the BOAT tag from tags — notes with links are not BOAT.
After the frontmatter, add a short German-language description paragraph (2–4 sentences). Keep any template section headers (## Zusammenfassung, ## Kernaussagen, ## Lieblingsfolgen, etc.).
Person body structure: short German bio paragraph, then ### Fakten (bullet list of key facts), then ### Ideen (empty, for future notes).
The image embed ![[filename.webp]] will be added after cover download (step 6) — do not add it manually.
Use inline Python (not the download scripts, which require manual list editing). Use Pillow if available for WebP output, otherwise JPEG.
Book — Open Library by title search:
import json, urllib.request, urllib.parse, io, os
query = "<title>"
url = f"https://openlibrary.org/search.json?title={urllib.parse.quote(query)}&limit=3"
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 VaultBot/1.0"})
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
docs = data.get("docs", [])
cover_id = next((d["cover_i"] for d in docs if d.get("cover_i")), None)
if cover_id:
img_url = f"https://covers.openlibrary.org/b/id/{cover_id}-L.jpg"
# download and save as WebP using PillowOutput path: 99 Meta/assets/books/<title> (<first author>).webp
Podcast — iTunes Search API:
import json, urllib.request, urllib.parse
query = "<podcast name>"
params = urllib.parse.urlencode({"term": query, "entity": "podcast", "limit": 5})
url = f"https://itunes.apple.com/search?{params}"
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 VaultBot/1.0"})
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
artwork = data["results"][0].get("artworkUrl600") if data["results"] else None
# download and save as WebP using PillowOutput path: 99 Meta/assets/podcasts/<title>.webp
TV show — TVMaze API:
import json, urllib.request, urllib.parse
query = "<show title>"
url = f"https://api.tvmaze.com/singlesearch/shows?q={urllib.parse.quote(query)}&embed=images"
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 VaultBot/1.0"})
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
image_url = data.get("image", {}).get("original") or data.get("image", {}).get("medium")
# download and save as WebP using PillowOutput path: 99 Meta/assets/series/<title>.webp
Movie — Wikipedia REST API (search for poster):
import json, urllib.request, urllib.parse
title = "<movie title>"
search_url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{urllib.parse.quote(title)}"
req = urllib.request.Request(search_url, headers={"User-Agent": "Mozilla/5.0 VaultBot/1.0"})
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
image_url = data.get("originalimage", {}).get("source") or data.get("thumbnail", {}).get("source")
# download and save as WebP using PillowOutput path: 99 Meta/assets/movies/<title>.webp
Country — Wikipedia REST API (flag or representative image):
import json, urllib.request, urllib.parse
title = "<country name>"
search_url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{urllib.parse.quote(title)}"
req = urllib.request.Request(search_url, headers={"User-Agent": "Mozilla/5.0 VaultBot/1.0"})
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
image_url = data.get("originalimage", {}).get("source") or data.get("thumbnail", {}).get("source")
# download and save as WebP using PillowOutput path: 99 Meta/assets/countries/<title>.webp
Person — Wikipedia REST API, then resize to max 800px on longest side:
import json, urllib.request, urllib.parse
name = "<person name>"
search_url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{urllib.parse.quote(name)}"
req = urllib.request.Request(search_url, headers={"User-Agent": "Mozilla/5.0 VaultBot/1.0"})
with urllib.request.urlopen(req, timeout=15) as resp:
data = json.loads(resp.read())
image_url = data.get("originalimage", {}).get("source") or data.get("thumbnail", {}).get("source")
# download, resize to max 800px longest side, save as WebP using PillowOutput path: 99 Meta/assets/people/<title>.webp
City — Wikipedia REST API:
import json, urllib.request, urllib.parse
title = "<city name>"
search_url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{urllib.parse.quote(title)}"
req = urllib.request.Request(search_url, headers={"User-Agent": "Mozilla/5.0 VaultBot/1.0"})
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
image_url = data.get("originalimage", {}).get("source") or data.get("thumbnail", {}).get("source")
# download and save as WebP using PillowOutput path: 99 Meta/assets/cities/<title>.webp
After successful download:
photo: <filename> in frontmatter (Edit the file)![[<filename>]] to the note body using Edit (insert before the description paragraph) — do NOT use obsidian prepend, as it escapes the ! and produces \![[...]]If cover download fails, note it to the user and continue without a photo.
Also add the new entry to the appropriate download script so future re-runs include it:
| Type | Script |
|---|---|
| book | 99 Meta/scripts/download-book-covers.py |
| podcast | 99 Meta/scripts/download-podcast-covers.py |
| show | 99 Meta/scripts/download-series-covers.py |
| movie | 99 Meta/scripts/download-movie-covers.py |
Countries, cities, and persons have no dedicated download script — skip this step for those types.
Skip this step for `person`, `country`, and `city` — they are reference notes, not media being consumed.
For media types, append to today's daily note using the correct emoji and wikilink format:
| Type | Format |
|---|---|
| book | 📚 [[<title>]] angefangen zu lesen (0 %) |
| podcast | 🎙️ [[<title>]]: <episode/topic if known> |
| show | 📺 [[<title>]] <season>.<episode> |
| movie | 🎬 [[<title>]] |
obsidian daily:append content="<entry>" 2>/dev/null | grep -Ev "^(20[0-9]{2}-|Your Obsidian)"If the user provided season/episode or episode title in the arguments, use it; otherwise use a sensible placeholder or ask.
qmd update && qmd embedRun this after the note is created and finalized so it's immediately searchable.
Summarize what was done:
obsidian daily:append fails, tell the user the entry to add manuallyqmd embed takes too long, run it in the background with run_in_background: true~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.