Dylan Wright72 downloadsResolve attachment embeds from folders outside the vault — offload heavy files to external drives or extra cloud storage while wikilinks keep rendering.
"The map is not the territory." — Alfred Korzybski
An Obsidian plugin for vaults that outgrow their cloud storage. When an attachment embed fails to resolve inside the vault, the plugin quietly searches one or more folders outside the vault — an external drive, a second free cloud account, a NAS share — and renders the file from there. Your wikilinks keep working. Your sync quota breathes again.
A wikilink works like a map; the file lives in some territory. Obsidian assumes the territory sits inside the vault, and that assumption holds up beautifully — right until the vault syncs through a free cloud tier and the scans, videos, and voice memos blow past the quota.
At that point the standard advice forks in two directions: delete your history, or pay rent on it forever. Both answers treat the vault's folder boundary as a law of nature rather than a default setting. This plugin opens a third door. Move the heavy files anywhere you like — spread them across as many free accounts, drives, and shares as you care to maintain — and the embeds keep resolving as if nothing moved. The note never learns the difference.
Nothing else did this, so this had to exist. Now it does, and you can have it for free.
The plugin practices strict non-interference: if Obsidian can resolve an embed, the plugin does nothing at all. Only genuinely broken embeds get a second chance.
%%{init: {'flowchart': {'curve': 'basis'}}}%%
flowchart TD
E(["Embed appears: ![[diagram.png]]"]) --> V{"Does the vault<br/>resolve it?"}
V -->|yes| OB["Obsidian renders it normally —<br/>the plugin stays out of the way"]
V -->|no| M1{"External folder 1<br/>holds diagram.png?"}
M1 -->|yes| R
M1 -->|no| M2{"External folder 2? 3? …<br/>(in order, first match wins)"}
M2 -->|yes| R
M2 -->|no| IDX{"Subfolder search on?<br/>Check the filename index"}
IDX -->|found| R["Read the file → blob URL →<br/>render as image · video · audio · PDF<br/>(subtle dashed outline, optional)"]
IDX -->|not found| X["Leave the embed alone —<br/>broken links stay honestly broken"]
classDef terminal fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,stroke-width:2px;
classDef native fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:1px;
classDef decision fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:1px;
classDef hit fill:#dcfce7,stroke:#16a34a,color:#14532d,stroke-width:2px;
classDef miss fill:#fee2e2,stroke:#dc2626,color:#7f1d1d,stroke-width:1px;
class E terminal;
class OB native;
class V,M1,M2,IDX decision;
class R hit;
class X miss;
linkStyle 1 stroke:#2563eb,stroke-width:2px;
linkStyle 3,5,7 stroke:#16a34a,stroke-width:2px;
linkStyle 8 stroke:#dc2626,stroke-width:2px,stroke-dasharray:5 5;
Matching happens by filename, so ![[diagram.png]] finds diagram.png wherever it landed.
Obsidian's sizing suffix carries over intact: ![[diagram.png|400]] renders 400 px wide, ![[diagram.png|400x300]] pins both axes, and spaces around the pipe — ![[diagram.png | 400]] — parse the same way. Sized or not, nothing exceeds the width of the note it sits in. Unknown file types fall back to a download link.

The same external file, unsized and at |300. The dashed outline marks an embed that resolved from outside the vault; it traces the image at whatever size the link asked for.
Externally-resolved embeds deliberately avoid Obsidian's own embed classes (image-embed, media-embed, is-loaded) and carry only their own. Themes hang layout on those names — Minimal, for instance, restyles the wrapper around anything holding an .image-embed and drives its click-to-zoom off .image-embed:active — and an embed backed by a blob URL rather than a vault file can't honor the rest of that contract. Staying out of the namespace keeps external attachments predictable instead of half-styled. Target them with .internal-embed[data-external-attachment="true"] in a snippet if you want theme parity on your own terms.
The Alignment setting puts external media left (the default, matching how Obsidian places ordinary attachments), centered, or right. Any single embed overrides it by naming the direction in the wikilink — alone, ![[diagram.png|right]], or alongside a size in either order, ![[diagram.png|400|right]]. Obsidian ignores the extra segment, so a note carrying one still reads correctly with the plugin disabled.

Default left, then |250|center and |250|right overriding it on individual links. Left lines the image up with the text column, exactly where Obsidian puts an ordinary attachment.
Two sized embeds written on the same line render side by side, the way two ordinary images would:
![[chart-before.png|300]] ![[chart-after.png|300]]

Sizing is what makes that work — a sized embed shrink-wraps to its media, while an unsized one keeps the full width of the line and stacks.
Two things follow from how CSS places a shrink-wrapped element, and both are deliberate. Alignment lands on the line rather than on one image within it, so a |center anywhere in a row centers the whole row. And centering or right-aligning a sized embed takes effect only on a line holding nothing but embeds — mix an image into a sentence and it stays inline where you wrote it. The plugin checks the surrounding element before touching its alignment, because text-align inherits: steering anything wider would swing the note's paragraphs and headings around with the picture.
One researcher keeps a 46 GB vault: archive scans, seminar recordings, voice memos. The free sync tier holds 5 GB. Rather than pruning the archive or renting space for it forever, they cut the heavy files out of the vault, drop them into three folders their OS already mounts, and change not one line of any note:
%%{init: {'flowchart': {'curve': 'basis'}}}%%
flowchart LR
subgraph vault["📓 The vault — 180 MB, syncs in seconds"]
N1["Field note:<br/>![[ledger-1874.png|500]]"]
N2["Seminar note:<br/>![[seminar-04.mp4]]"]
end
N1 --> P{{"External Attachments<br/>looks the filename up"}}
N2 --> P
subgraph ext["🗄️ Outside the vault — 46 GB, syncs never"]
F1["1 · D:\VaultMedia<br/>(external drive)"]
F2["2 · Drive mirror<br/>(second free account)"]
F3["3 · NAS share<br/>(cold archive)"]
end
P --> F1 --> F2 --> F3
F1 -.->|hit| OUT
F2 -.->|hit| OUT
F3 -.->|hit| OUT
OUT(["Both notes render in place —<br/>the scan at 500 px, the video with controls.<br/>Quota untouched."])
classDef terminal fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,stroke-width:2px;
classDef note fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:1px;
classDef lookup fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:2px;
classDef folder fill:#dcfce7,stroke:#16a34a,color:#14532d,stroke-width:1px;
class N1,N2 note;
class P lookup;
class F1,F2,F3 folder;
class OUT terminal;
style vault fill:#eff6ff,stroke:#60a5fa,color:#1e3a8a;
style ext fill:#f0fdf4,stroke:#4ade80,color:#14532d;
linkStyle 0,1 stroke:#2563eb,stroke-width:2px;
linkStyle 2 stroke:#d97706,stroke-width:2px;
linkStyle 3,4 stroke:#16a34a,stroke-width:1px,stroke-dasharray:3 4;
linkStyle 5,6,7 stroke:#7c3aed,stroke-width:2px;
The sync client now carries 180 MB of text. The archive keeps growing wherever the researcher has room for it, spread across as many drives and free accounts as they care to add — and every wikilink written over the past five years still resolves.
%%{init: {'flowchart': {'curve': 'basis'}}}%%
flowchart LR
A["1 · Install the plugin<br/>and enable it"] --> B["2 · Settings →<br/>External Attachments →<br/>add external folder(s)"]
B --> C["3 · Move heavy attachments<br/>out of the vault<br/>into those folders"]
C --> D(["4 · Embeds keep rendering.<br/>Sync quota recovers."])
classDef step fill:#e0e7ff,stroke:#4f46e5,color:#312e81,stroke-width:1px;
classDef terminal fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,stroke-width:2px;
class A,B,C step;
class D terminal;
linkStyle 0,1,2 stroke:#4f46e5,stroke-width:2px;
D:\ObsidianAttachments, a Google Drive mirror, a Dropbox folder, anything your OS mounts as a directory. Order matters: the plugin checks folders top to bottom and stops at the first match. A status line under each path tells you whether the plugin can reach it.To split attachments across several free cloud services, install each service's desktop sync client, give each one a folder, and list all of those folders in settings. The plugin treats them as one big pool.
|left, |center, or |right segment on an individual wikilink overrides it.Every tool encodes assumptions; these deserve stating plainly rather than discovering painfully.
data.json and travel with whatever syncs your vault.Until the plugin lands in the community catalog, install with BRAT pointed at this repo, or copy main.js, manifest.json, and styles.css from a release into <vault>/.obsidian/plugins/external-attachments/.
Upgrading from a pre-release single-folder build? Your existing data.json migrates automatically — the old mount path becomes the first entry in the folder list.
npm install
npm run dev # esbuild watch mode
npm run build # typecheck + production bundle
npm test # resolver test suite — keep it green
The resolver (src/resolver.ts) holds the filesystem logic plus the pure link helpers — linkpathOf, mimeFor, parseEmbedSize — and imports nothing from Obsidian, so the test suite exercises all of it directly against real temporary directories. The plugin shell (src/main.ts) handles the DOM side: markdown post-processing for reading view, a MutationObserver for live preview, sizing the rendered media, and blob-URL lifecycle management so memory gets released when embeds leave the screen.
One stylistic note for contributors: the README and UI text keep to E-Prime — English without any form of "to be" — as a small tribute to Korzybski and Robert Anton Wilson, who argued that dropping "is" makes claims about what happened easier to tell apart from claims about what things really are. A plugin whose entire job involves the gap between a link and a file might as well practice what it renders.
MIT