Initial commit

This commit is contained in:
Henner M. Kruse
2026-08-04 00:14:42 +02:00
commit c774df7ad7
12 changed files with 705 additions and 0 deletions
+160
View File
@@ -0,0 +1,160 @@
---
name: obsidian-vault-kb
description: Reads an Obsidian vault as a knowledge base and uses its notes (Markdown files, frontmatter, tags, [[wikilinks]], folder structure) to answer questions and help solve problems, regardless of the vault's subject matter. Always use this skill when the user refers to their "vault", "notes", "docs", "wiki", "Obsidian", or a personal/topic-specific knowledge base — even if "Obsidian" isn't mentioned explicitly, as long as the context suggests a personal note collection as the information source (e.g. "check my notes", "what did I write about this", "according to my docs"). Covers both plain lookup and creating/updating notes, depending on the mode configured on first contact.
---
# Obsidian Vault as Knowledge Base
This skill makes an Obsidian vault (a directory of Markdown files with
Obsidian-typical structure: YAML frontmatter, `[[wikilinks]]`, `#tags`,
folder conventions) usable as a searchable knowledge base, regardless of what
topic the vault covers (e.g. technical documentation, project notes,
research or study notes).
## Workflow
### 1. Check configuration (always first)
Configuration lives in `~/.agent-skills/obsidian-vault-kb/config.json`. Before any
substantive action, check whether this file exists:
```bash
cat ~/.agent-skills/obsidian-vault-kb/config.json 2>/dev/null
```
**If the file exists:** Use the configuration from it (vault path(s), mode)
and go straight to step 2 (search). Do not ask again.
**If the file is missing (first call):** Briefly ask the user:
1. Path to the vault (absolute path, e.g. `/home/user/vaults/notes`).
Multiple vaults are possible — assign a short name to each.
2. Mode:
- `read-only` — read only, no write access whatsoever
- `append` — read + append new findings as a new, dated note (existing
notes are never modified)
- `maintain` — read + actively update/extend existing notes (e.g. status
fields, ongoing logs)
Then create the configuration file:
```bash
mkdir -p ~/.agent-skills/obsidian-vault-kb
cat > ~/.agent-skills/obsidian-vault-kb/config.json << 'EOF'
{
"vaults": [
{"name": "notes", "path": "/absolute/path/to/vault"}
],
"mode": "append"
}
EOF
```
Briefly confirm what was saved, then proceed. The user can change the
configuration at any time by explicitly saying so (e.g. "use a different
vault from now on", "switch to read-only mode") — then overwrite the file
with the new values.
### 2. Search the vault
For questions/problems, first use the folder structure as a topical
categorization, then dig into content. Details and example commands are in
`references/search-strategy.md` — consult it for any non-trivial search,
especially when using tags/frontmatter/wikilinks.
Short version of the approach:
1. Get an overview of the vault structure:
`<skill-dir>/scripts/vault_index.sh [vault-name]` — returns folders as
rough categories, note counts, existing frontmatter tags, and possible
index/MOC files.
2. Targeted full-text search with
`<skill-dir>/scripts/vault_search.sh "<query>" [vault-name] [subfolder]`
— scoped to the most plausible subfolder from step 1 where useful.
3. For a specific note, follow backlinks with
`<skill-dir>/scripts/vault_backlinks.sh "<note-name>" [vault-name]` to
find related notes that link to it.
4. Open matching files (via the `view`/read tool, not a shell command) to
read their frontmatter (tags, `status`, `aliases`) and content in full
once a promising candidate has been found.
These three scripts always resolve the vault path from
`~/.agent-skills/obsidian-vault-kb/config.json` themselves and refuse to operate
outside the configured vault — they never take a raw filesystem path as an
argument. This is intentional: it's what allows them (and only them) to be
safely whitelisted in Claude Code's permissions without granting broader
shell/filesystem access. See "Setup and permissions" below and
`references/search-strategy.md` for manual fallback commands (`find`, `rg`,
`grep`) for cases the scripts don't cover — those are not whitelisted and
will prompt for confirmation, by design.
Always start with a narrow, targeted search and only widen it if needed —
don't read the whole vault unstructured.
### 3. Formulate the answer
- Base answers explicitly on the notes found, briefly citing the source
(file name/note title); don't invent anything the vault doesn't support.
- If the vault doesn't contain relevant information, say so openly instead
of guessing, and clearly separate that from general domain knowledge if
used.
- For topics with exact detail values (e.g. configuration data, numbers,
names, version info), carry these over verbatim from the notes instead of
paraphrasing, since accuracy matters here.
### 4. Write notes (only in `append` or `maintain` mode)
- **`append`**: File the new finding/solution as a new file, e.g.
`<vault>/Notes/YYYY-MM-DD-<short-topic>.md` with frontmatter (`tags`,
`date`, possibly `related`) and wikilinks to affected topics/systems.
Never modify existing files.
- **`maintain`**: Extend an existing relevant note directly (e.g. append a
new "Update YYYY-MM-DD" section or update a status field in the
frontmatter) instead of creating a new file, when an existing note is the
right place for the content.
- **`read-only`**: Never create or modify files in the vault. If the user
asks anyway, briefly point out the current mode and ask whether the
configuration should be changed.
- Before writing, always briefly summarize what will be written; for more
substantial changes (`maintain` mode), explicitly confirm before modifying
an existing note.
## Setup and permissions
This SKILL.md and the `scripts/`/`references/` next to it are the
tool-neutral core of this skill (per the open Agent Skills spec at
agentskills.io) and don't assume any particular host tool. Configuration
always lives at the tool-neutral path
`~/.agent-skills/obsidian-vault-kb/config.json`, regardless of which agent
is running this skill.
Host-specific setup helpers live under `vendor/<tool>/` in this repo, not
here. For Claude Code specifically, `vendor/claude-code/plugins/obsidian-vault-kb/`
provides a `commands/setup.md` slash command — automatically namespaced by
Claude Code's plugin system and invoked as `/obsidian-vault-kb:setup` — plus
a `permissions-whitelist.template.json` for Claude Code's Bash permission
system. Only three exact, absolute-path commands are whitelisted there — the
three wrapper scripts in `scripts/` — never a generic `Bash(find:*)`,
`Bash(rg:*)`, `Bash(grep:*)`, or `Bash(cat:*)`. Those would allow reading
anywhere on the filesystem; the wrapper scripts instead resolve the vault
path only from `~/.agent-skills/obsidian-vault-kb/config.json` and refuse to
operate outside it, so whitelisting them by exact path doesn't widen access
beyond the configured vault.
If no host-specific setup helper exists for the tool currently running this
skill, or configuration is simply missing, run through the first-call flow
in step 1 above directly instead.
Nothing that writes into the vault or into the config file is whitelisted —
those keep prompting for confirmation every time, in `append`/`maintain`
mode, by design.
## Notes
- This skill doesn't replace the user's own folder-structure conventions —
the structure detected in step 1 determines how content is categorized,
not a fixed predefined schema.
- Don't automatically read binary files/attachments (`.pdf`, images) in the
vault, just reference them, unless the user explicitly asks for it.
- Ignore the vault's `.obsidian/` configuration directory, it contains no
knowledge content.
@@ -0,0 +1,88 @@
# Search Strategy for the Obsidian Vault (manual fallback)
The three wrapper scripts (`scripts/vault_index.sh`, `scripts/vault_search.sh`,
`scripts/vault_backlinks.sh`) cover the common cases and are the only
commands whitelisted by `/setup`. Use the raw commands below only when the
scripts genuinely don't cover what's needed (e.g. a one-off query shape they
don't support). These are **not** whitelisted, so each one will prompt the
user for confirmation — that's intentional, since they aren't confined to
the configured vault the way the scripts are.
## 1. Get an overview of the structure
```bash
# Folder structure (= topical categories per the user's own convention)
find <vault> -type d -not -path '*/.obsidian*' -not -path '*/.git*' | sort
# Do "index"/"MOC" (Map of Content) files exist? These often bundle links
# on a topic and are a good entry point.
find <vault> -iname '*index*.md' -o -iname '*moc*.md' -o -iname '*overview*.md'
```
## 2. Full-text search
Prefer `rg` (ripgrep), it's much faster and respects `.gitignore`:
```bash
# Basic search, case-insensitive, with file names and line numbers
rg -i -n "<search-term>" <vault> -g '!.obsidian' -g '!.git'
# Only file names of matches (for a quick overview)
rg -i -l "<search-term>" <vault>
# Combine multiple terms (AND via chained greps)
rg -i -l "term-a" <vault> | xargs rg -i -l "term-b"
```
Fallback without ripgrep:
```bash
grep -ril "<search-term>" <vault> --include='*.md'
```
## 3. Use frontmatter and tags
Obsidian frontmatter is a YAML block at the top of a file between `---`.
Tags can live there (`tags: [topic-a, topic-b]`) or inline in the text
(`#topic-a`).
```bash
# All notes with a specific tag in the frontmatter
rg -l '^tags:.*topic-a' <vault>
# All notes with a specific inline tag
rg -l '#topic-a\b' <vault>
# View the frontmatter of a single file (the first 15 lines are usually enough)
head -n 15 "<path-to-file>.md"
```
If the vault uses a consistent tag scheme (e.g. `status/open`,
`system/hostname`), use it as an additional filter dimension, not just
full text.
## 4. Wikilinks and backlinks
`[[NoteName]]` points to another note. For relationships between
topics/entities, backlinks are often more informative than full-text
search:
```bash
# Which notes link to "note-name"?
rg -n '\[\[note-name(\||\]\])' <vault>
# List all outgoing links of a specific note
rg -o '\[\[[^]]+\]\]' "<path-to-file>.md"
```
For troubleshooting-style questions, it's often worth following 12 link
hops from the topically closest note (e.g. hardware note → linked
network note → linked incident note) instead of only matching full text
in isolation.
## 5. Large vaults
If there are too many matches: first narrow down to the most plausible
subfolder from the structure overview (step 1), then search in detail.
Don't read every matching file in full by default — check grep context
first (`rg -C 3 ...`) and only open whole files when genuinely needed.
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# Shared helpers, sourced by the other scripts in this directory.
# Not meant to be executed directly, and not whitelisted on its own.
CONFIG_FILE="$HOME/.agent-skills/obsidian-vault-kb/config.json"
# Resolve a vault's absolute path by name from the config file.
# If no name is given and the config has exactly one vault, use that one.
# Never accepts a raw path from the caller — only a name looked up
# server-side (in the config file) — so a whitelisted script can't be
# pointed at an arbitrary directory outside the configured vault(s).
resolve_vault_path() {
local vault_name="${1:-}"
if [ ! -f "$CONFIG_FILE" ]; then
echo "Error: no configuration found at $CONFIG_FILE. Run /setup first." >&2
exit 1
fi
if ! command -v python3 >/dev/null 2>&1; then
echo "Error: python3 is required to parse the config file." >&2
exit 1
fi
python3 - "$CONFIG_FILE" "$vault_name" << 'PYEOF'
import json, sys
config_file, vault_name = sys.argv[1], sys.argv[2]
with open(config_file) as f:
cfg = json.load(f)
vaults = cfg.get("vaults", [])
if not vaults:
sys.stderr.write("Error: no vaults defined in config.\n")
sys.exit(1)
if vault_name:
match = [v for v in vaults if v.get("name") == vault_name]
if not match:
sys.stderr.write(f"Error: no vault named '{vault_name}' in config.\n")
sys.exit(1)
print(match[0]["path"])
elif len(vaults) == 1:
print(vaults[0]["path"])
else:
names = ", ".join(v.get("name", "?") for v in vaults)
sys.stderr.write(f"Error: multiple vaults configured ({names}); specify one by name.\n")
sys.exit(1)
PYEOF
}
# Validate that a resolved path is an existing, real directory and print
# its canonical form (resolves symlinks, blocks '..' tricks).
canonicalize_and_check_dir() {
local path="$1"
local real
real=$(realpath -e "$path" 2>/dev/null) || {
echo "Error: vault path '$path' does not exist." >&2
exit 1
}
if [ ! -d "$real" ]; then
echo "Error: vault path '$real' is not a directory." >&2
exit 1
fi
echo "$real"
}
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Find backlinks (notes containing [[note-name]]) within a configured vault.
#
# Usage: vault_backlinks.sh <note-name> [vault-name]
#
# The vault path always comes from
# ~/.agent-skills/obsidian-vault-kb/config.json — never from a raw argument.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=_lib.sh
source "$SCRIPT_DIR/_lib.sh"
NOTE_NAME="${1:?Usage: vault_backlinks.sh <note-name> [vault-name]}"
VAULT_NAME="${2:-}"
VAULT_RAW="$(resolve_vault_path "$VAULT_NAME")"
VAULT="$(canonicalize_and_check_dir "$VAULT_RAW")"
# Build a regex-safe literal for the note name (escape regex metachars).
ESCAPED_NAME=$(printf '%s' "$NOTE_NAME" | sed -e 's/[.[\*^$/]/\\&/g')
echo "== Notes linking to [[$NOTE_NAME]] in $VAULT =="
echo
if command -v rg >/dev/null 2>&1; then
rg -n "\[\[${ESCAPED_NAME}(\||\]\])" "$VAULT" \
-g '!.obsidian' -g '!.git' -g '*.md' || echo "(no backlinks found)"
else
grep -rn "\[\[${ESCAPED_NAME}" "$VAULT" --include='*.md' 2>/dev/null || echo "(no backlinks found)"
fi
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# Quick structured overview of a configured Obsidian vault:
# folder structure, note count per folder, frontmatter tags in use.
#
# Usage: vault_index.sh [vault-name]
# vault-name is optional if only one vault is configured.
# The path is always resolved from ~/.agent-skills/obsidian-vault-kb/config.json —
# this script never accepts a raw filesystem path as an argument.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=_lib.sh
source "$SCRIPT_DIR/_lib.sh"
VAULT_NAME="${1:-}"
VAULT_RAW="$(resolve_vault_path "$VAULT_NAME")"
VAULT="$(canonicalize_and_check_dir "$VAULT_RAW")"
echo "== Vault: $VAULT =="
echo
echo "== Folder structure =="
find "$VAULT" -type d \
-not -path '*/.obsidian*' -not -path '*/.git*' \
| sed "s|^$VAULT||" | sort
echo
echo "== Notes per top-level folder =="
find "$VAULT" -mindepth 1 -maxdepth 1 -type d \
-not -name '.obsidian' -not -name '.git' \
| while read -r dir; do
count=$(find "$dir" -name '*.md' | wc -l)
echo "$(basename "$dir"): $count"
done
echo
echo "== Frontmatter tags (field 'tags:') =="
if command -v rg >/dev/null 2>&1; then
rg -o --no-filename '^tags:\s*\[?([^]]*)\]?' -r '$1' "$VAULT" 2>/dev/null \
| tr ',' '\n' | sed 's/^\s*-\?\s*//; s/\s*$//' | sort -u | grep -v '^$' || true
else
grep -rho '^tags:.*' "$VAULT" --include='*.md' 2>/dev/null | sort -u || true
fi
echo
echo "== Possible index/MOC files =="
find "$VAULT" -iname '*index*.md' -o -iname '*moc*.md' -o -iname '*overview*.md' 2>/dev/null || true
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# Full-text search strictly confined to a configured vault.
#
# Usage: vault_search.sh <query> [vault-name] [subfolder]
# - query: search term (plain text, treated as a fixed string, not a
# shell-interpreted pattern)
# - vault-name: optional if only one vault is configured
# - subfolder: optional, relative subfolder to narrow the search; rejected
# if it tries to escape the vault (e.g. contains '..' or is
# an absolute path)
#
# The vault path always comes from
# ~/.agent-skills/obsidian-vault-kb/config.json — never from a raw argument —
# so this script can't be used to search outside the configured vault(s).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=_lib.sh
source "$SCRIPT_DIR/_lib.sh"
QUERY="${1:?Usage: vault_search.sh <query> [vault-name] [subfolder]}"
VAULT_NAME="${2:-}"
SUBFOLDER="${3:-}"
VAULT_RAW="$(resolve_vault_path "$VAULT_NAME")"
VAULT="$(canonicalize_and_check_dir "$VAULT_RAW")"
SEARCH_ROOT="$VAULT"
if [ -n "$SUBFOLDER" ]; then
case "$SUBFOLDER" in
/*|*..*)
echo "Error: subfolder must be a relative path within the vault (no '..' or absolute paths)." >&2
exit 1
;;
esac
CANDIDATE="$VAULT/$SUBFOLDER"
SEARCH_ROOT="$(realpath -e "$CANDIDATE" 2>/dev/null)" || {
echo "Error: subfolder '$SUBFOLDER' does not exist in the vault." >&2
exit 1
}
# Ensure the resolved path is still inside the vault after symlink resolution.
case "$SEARCH_ROOT" in
"$VAULT"/*|"$VAULT") ;;
*)
echo "Error: subfolder resolves outside the vault, refusing." >&2
exit 1
;;
esac
fi
echo "== Search root: $SEARCH_ROOT =="
echo
if command -v rg >/dev/null 2>&1; then
rg -i -n --fixed-strings -- "$QUERY" "$SEARCH_ROOT" \
-g '!.obsidian' -g '!.git' -g '*.md' || echo "(no matches)"
else
grep -ril -- "$QUERY" "$SEARCH_ROOT" --include='*.md' 2>/dev/null || echo "(no matches)"
fi