Integrate with Obsidian vaults through the Local REST API plugin.
Use this skill when the user wants to:
You are operating the Obsidian Vault Integration Skill, which enables Claude to interact with Obsidian vaults through the Local REST API plugin. This skill provides complete access to all 31 API endpoints for reading, searching, creating, updating, and managing Obsidian notes.
Critical Context: The Obsidian Local REST API runs on localhost (HTTPS port 27124 by default) and requires Bearer token authentication. All operations use standard HTTP methods with markdown content.
Each guardrail defines WHEN it applies, WHAT it checks, and HOW to confirm. These are reusable safety patterns that endpoints reference by ID.
ID: G1
Type: Config-based blocking
Applies to: All DELETE operations
Risk Level: ๐ด CRITICAL - Permanent data loss
Logic:
allowDelete from config (see Configuration section)allowDelete is false (default) โ BLOCK operation entirelyallowDelete is true โ Proceed to G2 (DELETE Confirmation)User Message (when blocked):
โ DELETE operations are disabled
To enable DELETE operations, set:
- Environment: OBSIDIAN_SKILL_ALLOW_DELETE=true
- .env file: allowDelete=true
- Config file: ~/.cc_obsidian/config.json โ "allowDelete": true
โ ๏ธ Enabling DELETE is permanent. Consider using backups.
Affected Endpoints: DELETE /active/, DELETE /vault/{filename}, DELETE /periodic/{period}/, DELETE /periodic/{period}/{year}/{month}/{day}/
ID: G2
Type: Mandatory user confirmation
Applies to: All DELETE operations (after G1 passes)
Risk Level: ๐ด CRITICAL - Irreversible
Logic:
DELETE (all caps)DELETE TODAY insteadCan be skipped? NO - NEVER (even with DANGEROUSLY_SKIP_CONFIRMATIONS=true)
Confirmation Template:
โ ๏ธ DESTRUCTIVE OPERATION - FILE DELETION
Operation: DELETE
Target: Projects/meeting-notes.md
Current size: 234 lines (1,456 words)
--- Content preview ---
Meeting Notes - Q4 Planning
[first 200 chars of content]
--- End preview ---
โ ๏ธ This operation CANNOT be undone.
โ ๏ธ A backup will be created (if backup.enabled: true)
Type 'DELETE' (in caps) to confirm, or 'cancel' to abort: _
For Current Period Notes:
โ ๏ธโ ๏ธโ ๏ธ DELETING TODAY'S DAILY NOTE โ ๏ธโ ๏ธโ ๏ธ
File: 2025-11-10.md (TODAY'S daily note)
Created: Today at 6:00 AM
Modified: 5 minutes ago
Size: 1,234 lines (5,678 words)
--- Content preview ---
2025-11-10 - Sunday
Morning Review
[content...]
--- End preview ---
โ ๏ธ This contains ALL of today's entries, tasks, and notes.
โ ๏ธ Consider archiving instead of deleting.
Type 'DELETE TODAY' (exactly) to confirm, or 'cancel' to abort: _
ID: G3
Type: User confirmation with existence check
Applies to: PUT operations on existing files
Risk Level: ๐ HIGH - Complete content replacement
Logic:
REPLACE (all caps)Can be skipped? YES (with DANGEROUSLY_SKIP_CONFIRMATIONS=true)
Confirmation Template (File Exists):
โ ๏ธ DESTRUCTIVE OPERATION - CONTENT REPLACEMENT
Operation: PUT (Replace All Content)
Target: Projects/meeting-notes.md
Current size: 234 lines (1,456 words)
--- Current content preview ---
[First 100 chars...]
...
[Last 100 chars...]
--- End current content ---
--- New content preview ---
[First 100 chars of new content...]
--- End new content ---
โ ๏ธ ALL existing content will be PERMANENTLY LOST.
โ ๏ธ A backup will be created (if backup.enabled: true)
Safer alternatives:
- POST /vault/Projects/meeting-notes.md โ Append to end
- PATCH /vault/Projects/meeting-notes.md โ Modify specific section
Type 'REPLACE' (in caps) to confirm, or 'cancel' to abort: _
Confirmation Template (New File):
Creating new file: Projects/new-note.md
--- Content preview ---
[First 200 chars...]
--- End preview ---
Confirm? (yes/no): _
Affected Endpoints: PUT /active/, PUT /vault/{filename}, PUT /periodic/{period}/, PUT /periodic/{period}/{year}/{month}/{day}/
ID: G4
Type: Operation-conditional confirmation
Applies to: PATCH operations with Operation: replace header
Risk Level: ๐ก MEDIUM - Section replacement
Logic:
Operation header valueOperation: append or Operation: prepend โ Skip guardrail (safe, additive)Operation: replace: Get current file content, parse and extract target section (if possible), show current section content, show new section content, require confirmation: type yesCan be skipped? YES (with DANGEROUSLY_SKIP_CONFIRMATIONS=true)
Confirmation Template:
โ ๏ธ PARTIAL CONTENT REPLACEMENT
Operation: PATCH with Operation=replace
Target Type: heading
Target: "Tasks"
File: Projects/meeting-notes.md
--- Current section content ---
Tasks section:
- [ ] Review Q4 goals
- [ ] Schedule team sync
--- End current section ---
--- New section content ---
Tasks section:
- [ ] Complete project proposal
--- End new section ---
โ ๏ธ This section will be completely replaced.
โ ๏ธ A backup will be created (if backup.enabled: true)
Type 'yes' to confirm, or 'no' to cancel: _
Affected Endpoints: PATCH /active/ (only when Operation: replace), PATCH /vault/{filename}, PATCH /periodic/{period}/, PATCH /periodic/{period}/{year}/{month}/{day}/
ID: G5
Type: Count-based confirmation
Applies to: Operations affecting >5 files
Risk Level: ๐ก MEDIUM - Multiple file modification
Logic:
yesCan be skipped? YES (with DANGEROUSLY_SKIP_CONFIRMATIONS=true)
Confirmation Template:
โ ๏ธ BULK OPERATION
Operation: POST (Append)
Affected files: 12
Files to be modified:
1. Archive/2025-01-01.md
2. Archive/2025-01-02.md
3. Archive/2025-01-03.md
[...]
12. Archive/2025-01-12.md
--- Content to append ---
[Preview of content being appended]
--- End content ---
Type 'yes' to proceed, or 'no' to cancel: _
Affected Operations: Any POST operation affecting >5 files, any batch PUT/PATCH/DELETE (though DELETE has G1+G2)
ID: G6
Type: Pre-operation context verification
Applies to: All operations on /active/ endpoints
Risk Level: ๐ก MEDIUM - User may not know active file
Logic:
GET /active/ to retrieve current active file pathCan be skipped? NO (always show active file path)
Confirmation Template:
The currently active file in Obsidian is:
๐ Projects/meeting-notes.md
Is this the file you want to [DELETE/modify/append to]? (yes/no): _
(If unsure, check your Obsidian window first)
Affected Endpoints: PUT /active/, POST /active/, PATCH /active/, DELETE /active/
ID: G7
Type: Global abort pattern
Applies to: All confirmation prompts
Risk Level: N/A (safety mechanism)
Logic:
Can be skipped? NO (always active)
Response:
โ Operation cancelled
No changes were made.
The skill uses a three-tier fallback system (first found wins):
Environment Variables (Highest Priority)
OBSIDIAN_SKILL_OBSIDIAN_SKILL_API_KEY="your-key"Project .env File (Second Priority)
.env in project rootapiKey=your-keyUser Config File (Lowest Priority)
~/.cc_obsidian/config.json{"apiKey": "your-key"}| Setting | Env Var | Config Key | Type | Default | Description |
|---|---|---|---|---|---|
| API Key | OBSIDIAN_SKILL_API_KEY |
apiKey |
string | required | Authentication token from Obsidian Local REST API plugin |
| API URL | OBSIDIAN_SKILL_API_URL |
apiUrl |
string | https://localhost:27124 |
Base URL for API |
| Allow DELETE | OBSIDIAN_SKILL_ALLOW_DELETE |
allowDelete |
boolean | false |
Enable DELETE operations |
| Backup Enabled | OBSIDIAN_SKILL_BACKUP_ENABLED |
backupEnabled |
boolean | true |
Auto-backup before destructive ops |
| Backup Directory | OBSIDIAN_SKILL_BACKUP_DIRECTORY |
backupDirectory |
string | ~/.cc_obsidian/backups |
Where to store backups |
| Backup Keep N | OBSIDIAN_SKILL_BACKUP_KEEP_LAST_N |
backupKeepLastN |
number | 5 |
Number of backups to keep |
| Skip Confirmations | OBSIDIAN_SKILL_DANGEROUSLY_SKIP_CONFIRMATIONS |
DANGEROUSLY_SKIP_CONFIRMATIONS |
boolean | false |
Skip confirmations (except DELETE) |
{
"apiKey": "your-api-key-here",
"apiUrl": "https://localhost:27124",
"allowDelete": false,
"backupEnabled": true,
"backupDirectory": "~/.cc_obsidian/backups",
"backupKeepLastN": 5,
"DANGEROUSLY_SKIP_CONFIRMATIONS": false
}
export OBSIDIAN_SKILL_API_KEY="your-key"
export OBSIDIAN_SKILL_API_URL="https://localhost:27124"
export OBSIDIAN_SKILL_ALLOW_DELETE=false
apiKey=your-api-key-here
apiUrl=https://localhost:27124
allowDelete=false
Use this pattern to load config values:
import os
import json
from pathlib import Path
from typing import Any
def get_config_value(key: str, default: Any = None) -> Any:
"""
Load configuration value with fallback priority.
Priority:
1. Environment variable: OBSIDIAN_SKILL_{KEY_UPPER}
2. Project .env file: {key}
3. User config: ~/.cc_obsidian/config.json
4. Default value
"""
# 1. Check environment variable with prefix
env_key = f"OBSIDIAN_SKILL_{key.upper()}"
env_value = os.getenv(env_key)
if env_value is not None:
# Convert string booleans
if env_value.lower() in ('true', 'false'):
return env_value.lower() == 'true'
# Convert numbers
if env_value.isdigit():
return int(env_value)
return env_value
# 2. Check project .env file (simplified - use python-dotenv in production)
dotenv_path = Path('.env')
if dotenv_path.exists():
with open(dotenv_path) as f:
for line in f:
line = line.strip()
if line.startswith('#') or '=' not in line:
continue
k, v = line.split('=', 1)
if k.strip() == key:
v = v.strip()
if v.lower() in ('true', 'false'):
return v.lower() == 'true'
if v.isdigit():
return int(v)
return v
# 3. Check user config file
config_path = Path.home() / '.cc_obsidian' / 'config.json'
if config_path.exists():
with open(config_path) as f:
config = json.load(f)
if key in config:
return config[key]
# 4. Return default
return default
# Usage examples
api_key = get_config_value('apiKey')
api_url = get_config_value('apiUrl', 'https://localhost:27124')
allow_delete = get_config_value('allowDelete', False)
Base URL: https://localhost:27124 (default)
Auth: Authorization: Bearer {API_KEY}
SSL: Use verify=False for self-signed localhost certs
Timeout: Always include timeout=10
Path Format Rules (CRITICAL):
folder/subfolder/note.md/folder/note.md is WRONG.md extensiontext/markdown - Markdown contentapplication/json - JSON data (tables, arrays)application/vnd.olrapi.note+json - Note with parsed metadataapplication/vnd.olrapi.dataview.dql+txt - Dataview DQL queryapplication/vnd.olrapi.jsonlogic+json - JsonLogic query/Purpose: Health check - returns server info Auth: NO (only unauthenticated endpoint) Risk: ๐ข None (read-only) Guardrails: None
/openapi.yamlPurpose: Returns OpenAPI specification Auth: YES Risk: ๐ข None (read-only) Guardrails: None
/obsidian-local-rest-api.crtPurpose: Returns SSL certificate Auth: YES Risk: ๐ข None (read-only) Guardrails: None
/active/Purpose: Get path and content of currently active file
Accept: text/markdown or application/vnd.olrapi.note+json
Risk: ๐ข None (read-only)
Guardrails: None
Use Case: Identify active file before operations
/active/Purpose: Append content to end of active file
Content-Type: text/markdown
Risk: ๐ข Low (append-only)
Guardrails: G6 (Active File Context), G5 (if bulk)
/active/Purpose: Replace entire content of active file
Content-Type: text/markdown or */*
Risk: ๐ HIGH - Overwrites ALL content
Guardrails: G6 (Active File Context) โ G3 (PUT Confirmation)
/active/Purpose: Modify specific section of active file
Headers: Operation (append/prepend/replace), Target-Type (heading/block/frontmatter), Target, Target-Delimiter (default: ::), Content-Type
Risk: ๐ก MEDIUM if Operation: replace, ๐ข Low if append/prepend
Guardrails: G6 (Active File Context) โ G4 (only when Operation: replace)
/active/Purpose: Delete currently active file Risk: ๐ด CRITICAL - Irreversible Guardrails: G6 (Active File Context) โ G1 (Permission Check) โ G2 (DELETE Confirmation)
/vault/{filename}Purpose: Read file content
Path: folder/subfolder/note.md (no leading slash)
Accept: text/markdown or application/vnd.olrapi.note+json
Risk: ๐ข None (read-only)
Guardrails: None
/vault/{filename}Purpose: Append to file (creates if doesn't exist)
Content-Type: text/markdown
Risk: ๐ข Low (append-only)
Guardrails: G5 (only if bulk >5 files)
/vault/{filename}Purpose: Create new file OR replace existing file entirely
Content-Type: text/markdown or */*
Risk: ๐ HIGH - Overwrites ALL content if file exists
Guardrails: G3 (PUT Confirmation)
/vault/{filename}Purpose: Modify specific section of file
Headers: Same as PATCH /active/ (see above)
Risk: ๐ก MEDIUM if Operation: replace, ๐ข Low if append/prepend
Guardrails: G4 (only when Operation: replace)
/vault/{filename}Purpose: Permanently delete file Risk: ๐ด CRITICAL - Irreversible Guardrails: G1 (Permission Check) โ G2 (DELETE Confirmation)
/vault/Purpose: List files in vault root
Returns: {"files": ["note.md", "folder/"]}
Risk: ๐ข None (read-only)
Guardrails: None
Note: Directories end with /
/vault/{pathToDirectory}/Purpose: List files in specific directory
Returns: {"files": ["note.md", "subfolder/"]}
Risk: ๐ข None (read-only)
Guardrails: None
Note: Empty directories not returned
Supports: daily, weekly, monthly, quarterly, yearly
/periodic/{period}/Purpose: Get current period note (today's daily, this week's weekly, etc.)
Accept: text/markdown or application/vnd.olrapi.note+json
Risk: ๐ข None (read-only)
Guardrails: None
/periodic/{period}/{year}/{month}/{day}/Purpose: Get historical periodic note for specific date Risk: ๐ข None (read-only) Guardrails: None
/periodic/{period}/Purpose: Append to current period note (creates if doesn't exist)
Content-Type: text/markdown
Risk: ๐ข Low (append-only)
Guardrails: G5 (only if bulk)
/periodic/{period}/{year}/{month}/{day}/Purpose: Append to historical periodic note (creates if doesn't exist)
Content-Type: text/markdown
Risk: ๐ข Low (append-only)
Guardrails: G5 (only if bulk)
/periodic/{period}/Purpose: Replace content of current period note
Content-Type: text/markdown or */*
Risk: ๐ HIGH - Overwrites today's/this week's note entirely
Guardrails: G3 (PUT Confirmation) with extra warning for current period
/periodic/{period}/{year}/{month}/{day}/Purpose: Replace content of historical periodic note
Content-Type: text/markdown or */*
Risk: ๐ HIGH - Overwrites historical note
Guardrails: G3 (PUT Confirmation)
/periodic/{period}/Purpose: Modify section of current period note
Headers: Same as PATCH /active/ (see above)
Risk: ๐ก MEDIUM if Operation: replace, ๐ข Low if append/prepend
Guardrails: G4 (only when Operation: replace)
/periodic/{period}/{year}/{month}/{day}/Purpose: Modify section of historical periodic note
Headers: Same as PATCH /active/ (see above)
Risk: ๐ก MEDIUM if Operation: replace, ๐ข Low if append/prepend
Guardrails: G4 (only when Operation: replace)
/periodic/{period}/Purpose: Delete current period note Risk: ๐ด CRITICAL - Deletes today's/this week's/this month's note Guardrails: G1 (Permission Check) โ G2 (DELETE Confirmation with extra prominent warning "THIS IS TODAY'S DAILY NOTE" and require "DELETE TODAY") Warning: At 11 PM, deleting daily note loses entire day's work
/periodic/{period}/{year}/{month}/{day}/Purpose: Delete historical periodic note Risk: ๐ด CRITICAL - Irreversible Guardrails: G1 (Permission Check) โ G2 (DELETE Confirmation with date clearly shown)
/search/simple/Purpose: Simple text search across vault
Body: {"query": "search term", "contextLength": 100}
Content-Type: application/json
Returns: Array of matches with context
Risk: ๐ข None (read-only)
Guardrails: None
/search/Purpose: Advanced search (Dataview DQL or JsonLogic) Content-Type:
application/vnd.olrapi.dataview.dql+txt for Dataview queriesapplication/vnd.olrapi.jsonlogic+json for JsonLogic queries
Body: Query string or JSON
Returns: Search results (only non-falsy results)
Risk: ๐ข None (read-only)
Guardrails: NoneDataview DQL Example:
TABLE
time-played AS "Time Played",
length AS "Length",
rating AS "Rating"
FROM #game
SORT rating DESC
JsonLogic Examples:
Find by frontmatter value:
{
"==": [
{"var": "frontmatter.myField"},
"myValue"
]
}
Find by tag:
{
"in": [
"myTag",
{"var": "tags"}
]
}
Custom JsonLogic Operators:
glob: [PATTERN, VALUE] - Match glob patternsregexp: [PATTERN, VALUE] - Match regular expressions/commands/Purpose: List all available Obsidian commands
Returns: {"commands": [{"id": "graph:open", "name": "Graph view: Open graph view"}]}
Risk: ๐ข None (read-only)
Guardrails: None
/commands/{commandId}/Purpose: Execute Obsidian command Risk: ๐ VARIABLE - Depends on command Guardrails: Command analysis with keyword detection (always show command name/description, require 'yes' for dangerous patterns: "delete", "remove", "clear", "erase", "destroy")
/open/{filename}Purpose: Open file in Obsidian UI (brings into focus)
Query Params: newLeaf=true to open in new tab
Risk: ๐ข None (UI operation only)
Guardrails: None
Note: Creates empty file if doesn't exist
import os
import requests
api_key = os.getenv('OBSIDIAN_SKILL_API_KEY')
api_url = os.getenv('OBSIDIAN_SKILL_API_URL', 'https://localhost:27124')
response = requests.get(
f'{api_url}/vault/Projects/meeting-notes.md',
headers={'Authorization': f'Bearer {api_key}'},
verify=False, # Required for self-signed localhost cert
timeout=10 # Always include timeout
)
if response.status_code == 200:
content = response.text
print(f"โ
File read successfully ({len(content)} chars)")
print(content)
elif response.status_code == 404:
print(f"โ File not found: Projects/meeting-notes.md")
else:
print(f"โ Error {response.status_code}: {response.text}")
import os
import requests
from pathlib import Path
def create_note(file_path: str, content: str):
"""Create new note with confirmation"""
api_key = os.getenv('OBSIDIAN_SKILL_API_KEY')
api_url = os.getenv('OBSIDIAN_SKILL_API_URL', 'https://localhost:27124')
# Check if file exists
check = requests.get(
f'{api_url}/vault/{file_path}',
headers={'Authorization': f'Bearer {api_key}'},
verify=False,
timeout=10
)
if check.status_code == 200:
# File exists - show warning (PUT would overwrite)
print(f"โ ๏ธ File already exists: {file_path}")
print("Use PUT to replace or POST to append")
return False
# File doesn't exist - safe to create
preview = content[:200] + "..." if len(content) > 200 else content
print(f"Creating new file: {file_path}")
print(f"Content preview:\n{preview}\n")
confirm = input("Confirm? (yes/no): ").strip().lower()
if confirm not in ['yes', 'y']:
print("โ Operation cancelled")
return False
# Create file
response = requests.put(
f'{api_url}/vault/{file_path}',
headers={
'Authorization': f'Bearer {api_key}',
'Content-Type': 'text/markdown'
},
data=content,
verify=False,
timeout=10
)
if response.status_code in [200, 201, 204]:
print(f"โ
File created: {file_path}")
return True
else:
print(f"โ Create failed: {response.status_code}")
print(f"Error: {response.text}")
return False
def search_notes(query: str, context_length: int = 100):
"""Search notes with context"""
api_key = os.getenv('OBSIDIAN_SKILL_API_KEY')
api_url = os.getenv('OBSIDIAN_SKILL_API_URL', 'https://localhost:27124')
response = requests.post(
f'{api_url}/search/simple/',
headers={
'Authorization': f'Bearer {api_key}',
'Content-Type': 'application/json'
},
json={
'query': query,
'contextLength': context_length
},
verify=False,
timeout=10
)
if response.status_code == 200:
results = response.json()
print(f"โ
Found {len(results)} matches for '{query}'")
for i, result in enumerate(results, 1):
filename = result.get('filename', 'unknown')
matches = result.get('matches', [])
print(f"\n{i}. {filename}")
for match in matches:
context = match.get('context', '')
print(f" {context}")
return results
else:
print(f"โ Search failed: {response.status_code}")
print(f"Error: {response.text}")
return []
def update_note_section(file_path: str, heading: str, new_content: str, operation: str = 'replace'):
"""Update specific section of note using PATCH"""
api_key = os.getenv('OBSIDIAN_SKILL_API_KEY')
api_url = os.getenv('OBSIDIAN_SKILL_API_URL', 'https://localhost:27124')
# If operation is 'replace', show current content and confirm
if operation == 'replace':
# Get current content
response = requests.get(
f'{api_url}/vault/{file_path}',
headers={'Authorization': f'Bearer {api_key}'},
verify=False,
timeout=10
)
if response.status_code == 200:
print(f"โ ๏ธ PARTIAL CONTENT REPLACEMENT")
print(f"Target: heading '{heading}' in {file_path}")
print(f"New content:\n{new_content}\n")
confirm = input("Type 'yes' to confirm: ").strip().lower()
if confirm != 'yes':
print("โ Operation cancelled")
return False
# Execute PATCH
response = requests.patch(
f'{api_url}/vault/{file_path}',
headers={
'Authorization': f'Bearer {api_key}',
'Operation': operation, # append, prepend, or replace
'Target-Type': 'heading',
'Target': heading,
'Content-Type': 'text/markdown'
},
data=new_content,
verify=False,
timeout=10
)
if response.status_code in [200, 204]:
print(f"โ
Section updated: {heading}")
return True
else:
print(f"โ PATCH failed: {response.status_code}")
print(f"Error: {response.text}")
return False
def append_to_daily_note(content: str, date: str = None):
"""
Append content to daily note
Args:
content: Content to append
date: Optional date (YYYY-MM-DD), defaults to today
"""
api_key = os.getenv('OBSIDIAN_SKILL_API_KEY')
api_url = os.getenv('OBSIDIAN_SKILL_API_URL', 'https://localhost:27124')
if date is None:
# Use current date endpoint
endpoint = f'{api_url}/periodic/daily/'
else:
# Use specific date endpoint
year, month, day = date.split('-')
endpoint = f'{api_url}/periodic/daily/{year}/{month}/{day}/'
response = requests.post(
endpoint,
headers={
'Authorization': f'Bearer {api_key}',
'Content-Type': 'text/markdown'
},
data=content,
verify=False,
timeout=10
)
if response.status_code in [200, 204]:
date_str = date or "today"
print(f"โ
Appended to daily note ({date_str})")
return True
else:
print(f"โ Append failed: {response.status_code}")
print(f"Error: {response.text}")
return False
def delete_file_with_guardrails(file_path: str):
"""Delete file with complete guardrail implementation"""
from typing import Any
# Helper function for config loading
def get_config_value(key: str, default: Any = None) -> Any:
"""Load configuration value with fallback priority"""
# Implementation from Configuration section
# ... (see Configuration Loading Implementation above)
pass
api_key = os.getenv('OBSIDIAN_SKILL_API_KEY')
api_url = os.getenv('OBSIDIAN_SKILL_API_URL', 'https://localhost:27124')
# G1: Check if DELETE is allowed
if not get_config_value('allowDelete', False):
print("โ DELETE operations are disabled.")
print("To enable DELETE operations, set one of:")
print(" - Environment variable: OBSIDIAN_SKILL_ALLOW_DELETE=true")
print(" - In .env file: allowDelete=true")
print(" - In ~/.cc_obsidian/config.json: \"allowDelete\": true")
return False
# G2: Get file content for preview
response = requests.get(
f"{api_url}/vault/{file_path}",
headers={'Authorization': f'Bearer {api_key}'},
verify=False,
timeout=10
)
if response.status_code == 200:
content = response.text
preview = content[:200] + "..." if len(content) > 200 else content
word_count = len(content.split())
line_count = len(content.splitlines())
else:
preview = "[Could not retrieve preview]"
word_count = "unknown"
line_count = "unknown"
# G2: Show confirmation prompt
print(f"""
โ ๏ธ DESTRUCTIVE OPERATION - FILE DELETION
Operation: DELETE
Target: {file_path}
Current size: {line_count} lines ({word_count} words)
--- Content preview ---
{preview}
--- End preview ---
โ ๏ธ This operation CANNOT be undone.
โ ๏ธ A backup will be created (if backup.enabled: true)
""")
# G2: Get confirmation (force_confirm=True means NEVER skip)
confirm = input("Type 'DELETE' (in caps) to confirm, or 'cancel' to abort: ").strip()
# G7: Check for abort keywords
if confirm.lower() in ["no", "cancel", "stop", "abort"]:
print("โ Operation cancelled")
return False
if confirm != "DELETE":
print("โ Confirmation failed - must type 'DELETE' exactly")
return False
# Execute deletion
response = requests.delete(
f"{api_url}/vault/{file_path}",
headers={'Authorization': f'Bearer {api_key}'},
verify=False,
timeout=10
)
if response.status_code == 204:
print(f"โ
File deleted: {file_path}")
return True
else:
print(f"โ Delete failed: {response.status_code}")
print(f"Error: {response.text}")
return False
Symptoms: requests.exceptions.ConnectionError, "Connection refused"
Cause: Obsidian Local REST API plugin not running
Resolution:
curl -k -H "Authorization: Bearer $OBSIDIAN_SKILL_API_KEY" https://localhost:27124/User Message:
โ Cannot connect to Obsidian Local REST API
Troubleshooting:
1. Is Obsidian running?
2. Is the Local REST API plugin enabled?
3. Check Settings โ Community Plugins โ Local REST API
4. Verify port: https://localhost:27124
Test connection:
curl -k -H "Authorization: Bearer $OBSIDIAN_SKILL_API_KEY" https://localhost:27124/
Symptoms: HTTP 401 status code, "Unauthorized" or "Invalid API key"
Cause: Missing or incorrect API key
Resolution:
echo $OBSIDIAN_SKILL_API_KEY.env file in project~/.cc_obsidian/config.jsonUser Message:
โ API key authentication failed
Your API key may be missing or incorrect.
To fix:
1. Get your API key from Obsidian:
Settings โ Community Plugins โ Local REST API โ Show API Key
2. Set in one of these locations:
- Environment variable: export OBSIDIAN_SKILL_API_KEY="your-key"
- Project .env: apiKey=your-key
- User config: ~/.cc_obsidian/config.json
Currently checking:
- OBSIDIAN_SKILL_API_KEY env var: [status]
- .env file: [status]
- config.json: [status]
Symptoms: HTTP 404 status code, "File does not exist"
Cause: File doesn't exist or path format incorrect
Resolution:
folder/note.md (no leading slash).md extension includedUser Message:
โ File not found: {path}
Check:
1. Path format: folder/subfolder/note.md (no leading slash)
2. File exists in vault
3. .md extension included
4. No typos in path
Correct format: โ
Projects/meeting-notes.md
Wrong format: โ /Projects/meeting-notes.md
Symptoms: HTTP 400 status code, "Bad Request"
Cause: Invalid parameters, malformed content, or incorrect headers
Resolution:
User Message:
โ Bad request: {details}
Common causes:
1. Missing or incorrect Content-Type header
2. Malformed JSON or markdown content
3. PATCH operation missing required headers:
- Operation: append/prepend/replace
- Target-Type: heading/block/frontmatter
- Target: identifier
4. Invalid target or ambiguous target
Symptoms: HTTP 405 status code, "Method not allowed"
Cause: Path references a directory instead of a file
Resolution:
folder/ โ folder/note.md/vault/{path}/ to list directory contentsUser Message:
โ Method not allowed: path references a directory
Your path points to a directory, but this operation requires a file.
Fix:
- Current path: Projects/ (directory)
- Correct path: Projects/meeting-notes.md (file)
To list directory contents, use: GET /vault/Projects/
folder/subfolder/note.md/folder/note.md is WRONGfolder/note.md not folder\note.mdnote.md not noteExamples:
Correct: Projects/Work/meeting-notes.md
Wrong: /Projects/Work/meeting-notes.md (leading slash)
Wrong: Projects/Work/meeting-notes (missing .md)
Wrong: Projects\Work\meeting-notes.md (backslashes)
[[Note Name]] # Link to note
[[Note Name|Display Text]] # Link with custom text
[[Note Name#Heading]] # Link to heading
[[Note Name#^block-id]] # Link to block
#tag # Simple tag
#nested/tag # Nested tag
#tag-with-dashes # Tag with dashes
Some content here ^block-id # Block with reference ID
![[Note Name]] # Embed note
![[Image.png]] # Embed image
![[Note#Heading]] # Embed section
> [!note]
> This is a note callout
> [!warning]
> This is a warning callout
> [!tip] Custom Title
> This is a tip with custom title
---
title: My Note
tags: [project, important]
created: 2025-11-10
status: in-progress
---
Content starts here
Found multiple options:
1. Projects/meeting-notes.md (modified today)
2. Archive/meeting-notes.md (modified last week)
Which file? (1/2): _
For multi-step operations:
Creating daily note...
โ
Daily note created
Appending tasks...
โ
Tasks added
Opening in Obsidian...
โ
Complete
Success: Clear, specific, actionable
โ
File created: Projects/meeting-notes.md
โ
Appended 3 tasks to daily note
โ
Searched 42 notes, found 5 matches
Failure: Clear error, cause, and fix
โ File not found: Projects/meeting.md
Cause: File doesn't exist
Fix: Create with PUT or check path
Immediately abort if:
Use the Task tool with subagent_type: general-purpose for these scenarios:
Scenario: User asks "Find all notes about project X" but you don't know exact file names
Reason: Searching vault may require multiple API calls, grepping results, filtering
Solution: Delegate to sub-agent
Example:
Task(
subagent_type="general-purpose",
description="Search Obsidian vault for notes",
prompt="""
Search the Obsidian vault for all notes related to "project X".
Steps:
1. Use POST /search/simple/ endpoint with query "project X"
2. Review results and extract relevant file paths
3. Return a list of file paths that match
API Config:
- Base URL: {api_url}
- Headers: Authorization: Bearer {api_key}
- Verify: False (self-signed cert)
Return format:
{
"matching_files": ["path/to/note1.md", "path/to/note2.md"],
"total_matches": 2
}
"""
)
Scenario: User asks "Summarize all my meeting notes from last month"
Reason: Reading many files consumes your context window
Solution: Delegate reading and initial processing to sub-agent, get summary back
Scenario: User asks "What's the best Dataview query to find X?"
Reason: May require trial-and-error with different query syntax
Solution: Let sub-agent experiment with queries, return working query
Scenario: User asks "Analyze my entire research note (5000 lines) and extract insights"
Reason: Large file content would consume significant context
Solution: Sub-agent reads and processes, returns condensed insights
DO NOT use sub-agents for:
When sub-agent returns, you (the skill) must:
Example Flow:
# User asks: "Delete all draft notes from 2023"
# Step 1: Delegate search to sub-agent
result = Task(
subagent_type="general-purpose",
description="Find draft notes from 2023",
prompt="Search vault for files with 'draft' in name and created in 2023, return list of paths"
)
# Step 2: Skill validates and shows confirmation (NOT delegated)
draft_files = result['files'] # ['drafts/2023-01-01.md', 'drafts/2023-01-15.md']
print(f"Found {len(draft_files)} draft files from 2023:")
for f in draft_files:
print(f" - {f}")
# Step 3: Skill applies G1 + G2 guardrails (DELETE confirmation)
# Step 4: Skill executes DELETE (NOT delegated)
NEVER SKIP THESE:
DELETE Operations: Check config (allowDelete), ALWAYS require explicit confirmation (type DELETE), NEVER skippable even with DANGEROUSLY_SKIP_CONFIRMATIONS=true
PUT Operations: Check if file exists first, show "ALL CONTENT WILL BE LOST" warning if exists, suggest safer alternatives (POST/PATCH), require explicit confirmation (type REPLACE)
PATCH Replace Operations: If Operation: replace, show current section content, require confirmation. If Operation: append or prepend, safe to proceed without confirmation
Bulk Operations: Count files, if >5 show list and require confirmation
Command Execution: GET command name/description first, check for dangerous keywords, require confirmation with command details
Active File Operations: GET /active/ first to show file path, display prominently, confirm with user before proceeding
Environment Variable Check: Always check os.getenv('OBSIDIAN_SKILL_DANGEROUSLY_SKIP_CONFIRMATIONS') at runtime. If set to 'true', skip confirmations where allowed (but NEVER for DELETE)
User Abort: IMMEDIATELY stop if user says "no", "cancel", "stop", "abort". Never proceed after negative response
verify=False for localhost HTTPS (self-signed cert).md extensionbackupEnabled: trueThis skill enables powerful automation - use it responsibly with proper guardrails!