#!/usr/bin/env bash # Full-text search strictly confined to a configured vault. # # Usage: vault_search.sh [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 [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 -g '!.obsidian' -g '!.git' -g '*.md' \ -- "$QUERY" "$SEARCH_ROOT" || echo "(no matches)" else grep -ril -- "$QUERY" "$SEARCH_ROOT" --include='*.md' 2>/dev/null || echo "(no matches)" fi