obsidian-security-basics
Implement secure Obsidian plugin development practices. Covers credential storage, input validation, XSS prevention, network security, URI handler safety, and Electron security. Use when handling user data, storing API keys, making network requests, or preparing for community plugin submission. Trigger with phrases like "obsidian security", "secure obsidian plugin", "obsidian data protection", "obsidian privacy", "obsidian api key storage".
Best use case
obsidian-security-basics is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Implement secure Obsidian plugin development practices. Covers credential storage, input validation, XSS prevention, network security, URI handler safety, and Electron security. Use when handling user data, storing API keys, making network requests, or preparing for community plugin submission. Trigger with phrases like "obsidian security", "secure obsidian plugin", "obsidian data protection", "obsidian privacy", "obsidian api key storage".
Teams using obsidian-security-basics should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/obsidian-security-basics/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How obsidian-security-basics Compares
| Feature / Agent | obsidian-security-basics | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Implement secure Obsidian plugin development practices. Covers credential storage, input validation, XSS prevention, network security, URI handler safety, and Electron security. Use when handling user data, storing API keys, making network requests, or preparing for community plugin submission. Trigger with phrases like "obsidian security", "secure obsidian plugin", "obsidian data protection", "obsidian privacy", "obsidian api key storage".
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
Related Guides
AI Agents for Coding
Browse AI agent skills for coding, debugging, testing, refactoring, code review, and developer workflows across Claude, Cursor, and Codex.
Best AI Skills for Claude
Explore the best AI skills for Claude and Claude Code across coding, research, workflow automation, documentation, and agent operations.
ChatGPT vs Claude for Agent Skills
Compare ChatGPT and Claude for AI agent skills across coding, writing, research, and reusable workflow execution.
SKILL.md Source
# Obsidian Security Basics
## Overview
Security practices for Obsidian plugin development. Plugins run with full vault filesystem access and can make arbitrary network requests inside Electron. Responsible development requires protecting credentials, sanitizing external data, validating URI handlers, minimizing permissions, and following Obsidian's plugin guidelines to avoid community submission rejection.
## Prerequisites
- Obsidian plugin development environment
- Understanding that `.obsidian/plugins/<id>/data.json` is synced by cloud services
- Awareness of [Obsidian Plugin Guidelines](https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines)
## Instructions
### Step 1: Credential Storage — Never in data.json
Plugin settings (`data.json`) live inside the vault and are synced by iCloud, Dropbox, Obsidian Sync, and Git. API keys stored here are effectively public.
```typescript
// BAD: API key stored in plugin settings (synced to cloud, committed to Git)
interface BadSettings {
apiKey: string; // This ends up in .obsidian/plugins/my-plugin/data.json
}
// GOOD: Use Electron's safeStorage for desktop (encrypted at OS level)
import { Platform } from 'obsidian';
export class SecureStorage {
private plugin: Plugin;
constructor(plugin: Plugin) { this.plugin = plugin; }
async storeSecret(key: string, value: string): Promise<void> {
if (Platform.isDesktopApp) {
// Electron's safeStorage uses OS keychain (Keychain on macOS, DPAPI on Windows)
const { safeStorage } = require('electron').remote || require('@electron/remote');
if (safeStorage.isEncryptionAvailable()) {
const encrypted = safeStorage.encryptString(value);
const data = await this.plugin.loadData() ?? {};
data[`_encrypted_${key}`] = encrypted.toString('base64');
await this.plugin.saveData(data);
return;
}
}
// Fallback for mobile or when encryption unavailable: prompt each session
// Store only in memory — never persisted
this.memoryStore.set(key, value);
}
async getSecret(key: string): Promise<string | null> {
if (Platform.isDesktopApp) {
const { safeStorage } = require('electron').remote || require('@electron/remote');
const data = await this.plugin.loadData();
const encrypted = data?.[`_encrypted_${key}`];
if (encrypted && safeStorage.isEncryptionAvailable()) {
return safeStorage.decryptString(Buffer.from(encrypted, 'base64'));
}
}
return this.memoryStore.get(key) ?? null;
}
private memoryStore = new Map<string, string>();
}
// Alternative: prompt user each session (simplest, most secure)
async onload() {
if (!this.apiKey) {
this.apiKey = await this.promptForApiKey();
}
}
```
### Step 2: Input Validation and XSS Prevention
Data from HTTP responses, clipboard, or URI handlers must be sanitized before rendering.
```typescript
// Sanitize HTML content before inserting into Obsidian views
function sanitizeHtml(input: string): string {
// Strip dangerous elements
input = input.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
input = input.replace(/<iframe[^>]*>[\s\S]*?<\/iframe>/gi, '');
input = input.replace(/<object[^>]*>[\s\S]*?<\/object>/gi, '');
input = input.replace(/<embed[^>]*>/gi, '');
// Strip event handlers
input = input.replace(/\bon\w+\s*=\s*"[^"]*"/gi, '');
input = input.replace(/\bon\w+\s*=\s*'[^']*'/gi, '');
// Strip javascript: URIs
input = input.replace(/href\s*=\s*"javascript:[^"]*"/gi, 'href="#"');
return input;
}
// For plain text in DOM elements — escape instead of strip
function escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Safe DOM creation (preferred in Obsidian)
// Use createEl with text content — Obsidian escapes automatically
container.createEl('p', { text: userInput }); // Safe — text is escaped
container.createEl('p').innerHTML = userInput; // DANGEROUS — raw HTML injection
// For markdown content from external sources
function sanitizeMarkdown(md: string): string {
// Remove HTML blocks that could contain scripts
md = md.replace(/<script[\s\S]*?<\/script>/gi, '');
// Remove image onerror handlers
md = md.replace(/onerror\s*=\s*["'][^"']*["']/gi, '');
// Limit length to prevent DoS
if (md.length > 500_000) md = md.substring(0, 500_000);
return md;
}
```
### Step 3: Secure URI Handler Registration
Obsidian's `registerObsidianProtocolHandler` lets external apps trigger plugin actions via `obsidian://` URIs. Validate all parameters.
```typescript
this.registerObsidianProtocolHandler('myplugin', async (params) => {
// Whitelist allowed actions
const ALLOWED_ACTIONS = ['open', 'create', 'search'] as const;
type Action = typeof ALLOWED_ACTIONS[number];
const action = params.action as string;
if (!ALLOWED_ACTIONS.includes(action as Action)) {
new Notice(`Invalid action: ${action}`);
return;
}
// Sanitize file paths — prevent directory traversal
const path = params.path?.replace(/\.\./g, '').replace(/^\//, '');
if (!path) {
new Notice('Missing path parameter');
return;
}
// Validate path is within vault
const normalized = normalizePath(path);
if (normalized.includes('..') || normalized.startsWith('/')) {
new Notice('Invalid path');
return;
}
// Limit content length
const content = params.content?.substring(0, 100_000) ?? '';
switch (action as Action) {
case 'open': {
const file = this.app.vault.getAbstractFileByPath(normalized);
if (file instanceof TFile) {
await this.app.workspace.getLeaf().openFile(file);
} else {
new Notice(`File not found: ${normalized}`);
}
break;
}
case 'create': {
await this.app.vault.create(normalized, content);
new Notice(`Created: ${normalized}`);
break;
}
case 'search': {
// Use Obsidian's built-in search
(this.app as any).internalPlugins.plugins['global-search']
?.instance.openGlobalSearch(content);
break;
}
}
});
```
### Step 4: Secure Network Requests
```typescript
import { requestUrl, RequestUrlParam } from 'obsidian';
// Always use Obsidian's requestUrl — it respects proxy settings and CORS
async function secureFetch(url: string, options?: Partial<RequestUrlParam>): Promise<any> {
// Enforce HTTPS
if (!url.startsWith('https://')) {
throw new Error('Only HTTPS requests are allowed');
}
// Allowlist domains (prevents SSRF if URL comes from user input)
const ALLOWED_DOMAINS = ['api.example.com', 'cdn.example.com'];
const urlObj = new URL(url);
if (!ALLOWED_DOMAINS.includes(urlObj.hostname)) {
throw new Error(`Domain not allowed: ${urlObj.hostname}`);
}
const response = await requestUrl({
url,
method: 'GET',
headers: {
'User-Agent': 'ObsidianPlugin/1.0',
...options?.headers,
},
...options,
});
if (response.status < 200 || response.status >= 300) {
throw new Error(`HTTP ${response.status}: ${url}`);
}
return response.json;
}
// Never log or display full API responses — they may contain PII
function redactForLogging(data: any): any {
const redacted = { ...data };
const sensitiveKeys = ['apiKey', 'token', 'password', 'secret', 'authorization'];
for (const key of Object.keys(redacted)) {
if (sensitiveKeys.some(s => key.toLowerCase().includes(s))) {
redacted[key] = '[REDACTED]';
}
}
return redacted;
}
```
### Step 5: Permission Minimization
```typescript
// manifest.json — only set isDesktopOnly if you actually need Electron APIs
{
"isDesktopOnly": false
// Obsidian has no granular permission model in manifest.json.
// The review team evaluates your code for:
// - Network requests: must be essential to plugin function
// - Filesystem access outside vault: strongly discouraged
// - No telemetry/analytics without explicit user consent
// - No remote code loading (eval, new Function, loading JS from URL)
}
// At runtime: guard platform-specific code
import { Platform, FileSystemAdapter } from 'obsidian';
function getVaultBasePath(): string | null {
if (this.app.vault.adapter instanceof FileSystemAdapter) {
return this.app.vault.adapter.getBasePath();
// IMPORTANT: never access files outside this basePath
}
return null; // Mobile — no filesystem access outside vault
}
// Guard Electron APIs
if (Platform.isDesktopApp) {
// Safe to use: require('electron'), child_process, etc.
} else {
// Mobile: these APIs don't exist — provide fallback or disable feature
}
```
### Step 6: Plugin Review Rejection Checklist
Obsidian's plugin review team will reject plugins for these violations:
```typescript
// REJECTED: eval() or dynamic code execution
eval(userInput); // Never
new Function('return ' + code)(); // Never
document.createElement('script'); // Never for external scripts
// REJECTED: remote code loading
const script = document.createElement('script');
script.src = 'https://cdn.example.com/lib.js'; // Load at build time instead
// REJECTED: console.log in production
console.log('user data:', settings); // Remove before submission
// REJECTED: unencrypted credential storage
this.saveData({ apiKey: 'sk-abc123' }); // Use SecureStorage (Step 1)
// REJECTED: undisclosed network requests
fetch('https://analytics.example.com/track', { body: ... }); // No hidden telemetry
// APPROVED alternatives:
// - Bundle dependencies with esbuild (no runtime loading)
// - Use a debug flag for console statements
// - Document all network requests in README
// - Get explicit consent before any data leaves the device
```
## Output
- SecureStorage class using Electron's safeStorage for encrypted credential storage
- HTML and markdown sanitization for all external content
- URI handler with action whitelist and path validation
- Secure network request wrapper with HTTPS enforcement and domain allowlist
- Platform-specific guards for desktop/mobile code paths
- Plugin review rejection checklist with approved alternatives
## Error Handling
| Issue | Cause | Solution |
|-------|-------|----------|
| API key synced to cloud | Stored in `data.json` | Use `SecureStorage` with Electron `safeStorage` |
| XSS in note preview | Unsanitized external HTML | Use `createEl` with `text` property, or `sanitizeHtml` |
| Directory traversal via URI | Unvalidated path parameter | Strip `..`, normalize, validate within vault |
| SSRF from user-provided URL | No domain allowlist | Validate against ALLOWED_DOMAINS before `requestUrl` |
| Plugin rejected on review | `eval`, `console.log`, or telemetry | Follow rejection checklist (Step 6) |
| `safeStorage` unavailable | Older Electron version or mobile | Fall back to per-session prompt |
## Examples
### Content Security for Custom Views
```typescript
// When rendering external content in an ItemView
async onOpen() {
const externalData = await this.fetchData();
const container = this.containerEl.children[1];
container.empty();
// Safe: text content is escaped by createEl
container.createEl('h3', { text: externalData.title });
container.createEl('p', { text: externalData.summary });
// If you must render HTML, sanitize first
const safeHtml = sanitizeHtml(externalData.htmlContent);
const htmlContainer = container.createEl('div');
htmlContainer.innerHTML = safeHtml;
}
```
### Audit Your Plugin for Security Issues
```bash
# Quick security audit of plugin source code
grep -rn 'eval(\|new Function(' src/ --include="*.ts" && echo "FAIL: dynamic code execution"
grep -rn 'innerHTML\s*=' src/ --include="*.ts" && echo "WARN: check for XSS"
grep -rn 'console\.log' src/ --include="*.ts" | grep -v '// DEBUG' && echo "WARN: console.log in prod"
grep -rn 'apiKey\|secret\|password' src/ --include="*.ts" && echo "CHECK: credential handling"
echo "Done."
```
## Resources
- [Obsidian Plugin Guidelines](https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines)
- [Electron Security](https://www.electronjs.org/docs/latest/tutorial/security)
- [OWASP XSS Prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)
- [Obsidian requestUrl API](https://docs.obsidian.md/Reference/TypeScript+API/requestUrl)
## Next Steps
For production readiness checks, see `obsidian-prod-checklist`.
For deployment and community submission, see `obsidian-deploy-integration`.Related Skills
performing-security-testing
Test automate security vulnerability testing covering OWASP Top 10, SQL injection, XSS, CSRF, and authentication issues. Use when performing security assessments, penetration tests, or vulnerability scans. Trigger with phrases like "scan for vulnerabilities", "test security", or "run penetration test".
checking-session-security
Analyze session management implementations to identify security vulnerabilities in web applications. Use when you need to audit session handling, check for session fixation risks, review session timeout configurations, or validate session ID generation security. Trigger with phrases like "check session security", "audit session management", "review session handling", or "session fixation vulnerability".
finding-security-misconfigurations
Configure identify security misconfigurations in infrastructure-as-code, application settings, and system configurations. Use when you need to audit Terraform/CloudFormation templates, check application config files, validate system security settings, or ensure compliance with security best practices. Trigger with phrases like "find security misconfigurations", "audit infrastructure security", "check config security", or "scan for misconfigured settings".
responding-to-security-incidents
Analyze and guide security incident response, investigation, and remediation processes. Use when you need to handle security breaches, classify incidents, develop response playbooks, gather forensic evidence, or coordinate remediation efforts. Trigger with phrases like "security incident response", "ransomware attack response", "data breach investigation", "incident playbook", or "security forensics".
analyzing-security-headers
Analyze HTTP security headers of web domains to identify vulnerabilities and misconfigurations. Use when you need to audit website security headers, assess header compliance, or get security recommendations for web applications. Trigger with phrases like "analyze security headers", "check HTTP headers", "audit website security headers", or "evaluate CSP and HSTS configuration".
generating-security-audit-reports
Generate comprehensive security audit reports for applications and systems. Use when you need to assess security posture, identify vulnerabilities, evaluate compliance status, or create formal security documentation. Trigger with phrases like "create security audit report", "generate security assessment", "audit security posture", or "PCI-DSS compliance report".
workhuman-security-basics
Workhuman security basics for employee recognition and rewards API. Use when integrating Workhuman Social Recognition, or building recognition workflows with HRIS systems. Trigger: "workhuman security basics".
wispr-security-basics
Wispr Flow security basics for voice-to-text API integration. Use when integrating Wispr Flow dictation, WebSocket streaming, or building voice-powered applications. Trigger: "wispr security basics".
windsurf-security-basics
Apply Windsurf security best practices for workspace isolation, data privacy, and secret protection. Use when securing sensitive code from AI indexing, configuring telemetry, or auditing Windsurf security posture. Trigger with phrases like "windsurf security", "windsurf secrets", "windsurf privacy", "windsurf data protection", "codeiumignore".
webflow-security-basics
Apply Webflow API security best practices — token management, scope least privilege, OAuth 2.0 secret rotation, webhook signature verification, and audit logging. Use when securing API tokens, implementing least privilege access, or auditing Webflow security configuration. Trigger with phrases like "webflow security", "webflow secrets", "secure webflow", "webflow API key security", "webflow token rotation".
vercel-security-basics
Apply Vercel security best practices for secrets, headers, and access control. Use when securing API keys, configuring security headers, or auditing Vercel security configuration. Trigger with phrases like "vercel security", "vercel secrets", "secure vercel", "vercel headers", "vercel CSP".
veeva-security-basics
Veeva Vault security basics for REST API and clinical operations. Use when working with Veeva Vault document management and CRM. Trigger: "veeva security basics".