Loading...
Loading...
A guide for Obsidian plugin developers on security transparency, the SECURITY.md specification, and best practices for building privacy-respecting plugins.
When a developer submits a plugin to the Obsidian community registry, it undergoes a manual code review before approval. After that initial review, all subsequent updates ship directly to users without further review. This means users rely on trust — trust that the developer hasn't introduced behavior they wouldn't consent to.
Developers can get ahead of this by proactively disclosing what their plugin does. A SECURITY.md file in your repository tells users and automated tools exactly what data your plugin accesses, where it sends it, and why.
Place a SECURITY.md file in the root of your plugin repository. It uses YAML frontmatter for metadata and five required sections that describe your plugin's behavior. If a section doesn't apply, write None. This extends Obsidian's plugin security guidance and developer policies.
--- plugin-id: your-plugin-id version: 1.0.0 last-updated: YYYY-MM-DD --- # Security ## Network Activity | Host | Purpose | Data Sent | Direction | | --- | --- | --- | --- | | example.com | Describe purpose | Describe data | outbound | ## Data Collection | Data Type | Scope | Purpose | | --- | --- | --- | | Markdown files | Active file only | Describe purpose | ## Third-Party Services | Service | Purpose | Data Shared | | --- | --- | --- | | Service Name | Describe purpose | Describe data | ## Permissions - Describe any non-standard permissions used ## Data Storage | What | Where | Encrypted | | --- | --- | --- | | Describe data | Local plugin settings | No |
Table of every external host your plugin contacts.
Columns: Host (the domain), Purpose (why it's contacted), Data Sent (what leaves the device), Direction(inbound or outbound). List every host, even if it only fetches data. This is the most important section for Plugin Observer's cross-referencing.
What vault or user data your plugin reads and why.
Columns: Data Type (e.g., "Markdown files", "Frontmatter"), Scope (e.g., "Active file only", "All files", "Specific folder"), Purpose (why the data is needed).
External APIs, SDKs, or services your plugin integrates.
Columns: Service (name of the service), Purpose (what it does for the plugin), Data Shared (what data reaches the service).
Capabilities used beyond standard Obsidian plugin APIs.
A bulleted list. Common entries: filesystem access outside the vault (via fs module), shell commands (via child_process), clipboard access, or system-level operations. If your plugin only uses the standard Obsidian API, write None.
Where your plugin persists data and whether it's encrypted.
Columns: What (what data is stored), Where (local plugin settings, remote server, etc.), Encrypted (Yes/No, and method if yes).
Beyond SECURITY.md, these practices help build plugins that respect user privacy and score well on Plugin Observer.
Minimize vault access scope — request only the files and data you actually need.
If your plugin requires eval() or new Function() (e.g., for user-written queries), this will be detected as a user-invoked capability rather than a security threat, as long as the input doesn't originate from a network source. Avoid eval() when simpler alternatives exist.
Prefer letting users configure endpoints via settings rather than hardcoding URLs. User-configured endpoints are scored as capabilities, not behaviors, resulting in more favorable scores.
Pin your dependencies and audit them regularly. High dependency counts (15+) and known risky packages are flagged.
Use recognized hosts when possible. Plugin Observer maintains a registry of known services (cloud AI, CDNs, code hosting) that don't trigger privacy escalation.
Prefer standard Obsidian APIs (requestUrl, vault.read) over raw Node.js modules (fs, http, child_process).
Keep your SECURITY.md up to date with each release. A stale disclosure is less useful than a current one.
A stale SECURITY.md reduces its value. Add this GitHub Actions workflow to your plugin repository to automatically bump the version and last-updated fields in your SECURITY.md every time you push a release tag.
Save as .github/workflows/bump-security-md.yml in your plugin repository.
name: Bump SECURITY.md
on:
push:
tags:
- "[0-9]*"
- "v[0-9]*"
permissions:
contents: write
jobs:
bump:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.repository.default_branch }}
- name: Update SECURITY.md frontmatter
run: |
TAG="${{ github.ref_name }}"
VERSION="${TAG#v}"
TODAY="$(date +%Y-%m-%d)"
if [ ! -f SECURITY.md ]; then
echo "SECURITY.md not found, skipping"
exit 0
fi
sed -i "s/^version: .*/version: ${VERSION}/" SECURITY.md
sed -i "s/^last-updated: .*/last-updated: ${TODAY}/" SECURITY.md
- name: Commit and push
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add SECURITY.md
git diff --cached --quiet && exit 0
git commit -m "chore: bump SECURITY.md to ${TAG#v}"
git pushTriggers on both plain semver tags (1.2.3) and v-prefixed tags (v1.2.3).
Strips the v prefix before writing to the frontmatter, so the version always matches manifest.json.
Skips gracefully if SECURITY.md doesn't exist yet.
Commits only if the file actually changed — no empty commits.
When Plugin Observer analyzes your plugin, it looks for a SECURITY.md file in your repository root. If found:
Findings from static analysis are cross-referenced against your disclosures. If you've declared a host in Network Activity and we detect a call to that host, the finding is marked as "Disclosed."
Disclosed findings are still visible — nothing is hidden. The annotation adds context: this behavior was acknowledged by the developer.
Plugin Observer cross-references your SECURITY.md disclosures against the capability/behavior classification. Disclosed capabilities are marked as such in the plugin's analysis report.
If the version in your SECURITY.md frontmatter doesn't match the analyzed version, the disclosure is marked as stale. Stale disclosures are displayed but don't influence annotations.
Plugins without a SECURITY.md receive no penalty. This is an opt-in transparency mechanism.
Show your plugin's privacy score in your repository README with a shields.io badge. The badge displays the current score with color coding (green for 75+, yellow for 45+, red below 45) and links to your plugin's detail page on Plugin Observer.
Score 75+
Score 45–74
Score <45
Replace PLUGIN_ID with your plugin's Obsidian community registry ID (e.g., obsidian-git).
[](https://plugin-observer.com/plugin/PLUGIN_ID)
The badge updates automatically when your plugin is re-analyzed. Shields.io caches the result for a few minutes.
Your plugin ID is the same ID used in the Obsidian community plugin registry.
If your plugin hasn't been analyzed yet, the badge shows "pending" in grey.
Plugin Observer attempts to reproduce every plugin release from source and compare the output byte-for-byte against the artifact published on GitHub. Plugins with verified builds show a green badge, giving users confidence that the code they reviewed is the code they're running.
Follow these practices to maximize the chance your builds verify successfully:
Commit a lockfile — package-lock.json, pnpm-lock.yaml, or yarn.lock. Without a lockfile, dependency versions are resolved at build time and will differ across environments.
Use deterministic build tools. esbuild and rollup are deterministic by default: given the same inputs they produce identical outputs. Avoid build steps that embed timestamps, random identifiers, or non-deterministic file ordering.
Don't manually modify main.js before uploading it to a release. Any post-build edits will cause a divergence between the source-derived artifact and the published one.
Use CI/CD to build releases. A GitHub Actions release workflow guarantees that the artifact attached to each release tag was produced directly from the tagged source — no manual steps, no local environment differences.
Include a .nvmrc file or an engines.node field in package.json. Plugin Observer uses this to select the correct Node version when reproducing your build. Mismatched Node versions are a common cause of unavailable build status.