rocketBANG119 downloadsPick an image, upload it to Cloudflare R2, and insert it inline or set it as the note's image property.
Obsidian plugin: adds commands to pick an image on your device, upload it to Cloudflare R2, and drop it into the current note — either as an inline embed or as a frontmatter property.
Works on desktop and mobile (signing is done on-device with the Web Crypto API; no SDKs, no CORS setup needed on the bucket).
 at the cursor.image frontmatter property (name configurable) to the public URL.With Obsidian's CLI enabled, image-upload:upload does the same job without a file picker, and writes the result straight into a note:
for f in ~/Photos/*.jpg; do
obsidian image-upload:upload src="$f" file="Dinner" append height=500
done
Use file=/path= with append or property to say where the URL should end up. Without them the upload still happens, but the URL is currently lost — Obsidian stops waiting for a CLI handler after a few tens of milliseconds, well before an upload finishes, so the returned URL never reaches stdout and can't be piped or captured. Failures do get reported, so a silent run is a successful one.
| Flag | Meaning |
|---|---|
src=<path> |
Image to upload — vault-relative, or an absolute path. Required. |
max=<px> |
Longest edge to downscale to, overriding the setting. 0 uploads full size. |
quality=<1-100> |
Encoder quality, overriding the setting. |
file=<name> / path=<path> |
Note to write into, resolved like other Obsidian CLI commands. |
append |
Append the embed to that note. |
property |
Set that note's image property to the URL. |
alt=<text> |
Alt text for the embed. Defaults to the note name. |
width=<px> |
Display width for the embed. |
height=<px> |
Display height; the width follows from the image's aspect ratio. |
height= is the useful one for a batch of mixed portrait and landscape shots — every image ends up the same height down the note.
Images are downscaled so their longest edge fits Maximum image size (1600px by default) and re-encoded at Image quality (85). This applies to the commands and the CLI alike. A 12MP phone photo typically lands around a tenth of its original file size, with enough resolution left for a HiDPI screen. Set the maximum to 0 to upload untouched.
JPEG, PNG, and WebP are resized; anything else uploads byte-for-byte. PNGs stay lossless, so quality doesn't apply to them. Resizing happens on a canvas, which bakes in EXIF rotation and drops the remaining metadata. If an image can't be decoded it's uploaded as-is rather than failing.
https://<account-id>.r2.cloudflarestorage.com), bucket name, path template, and the public base URL.Object keys follow the path template (default /{year}/{mon}/{day}/{filename}), with a short random suffix added to the filename to avoid collisions.
Store credentials in the keychain picks between the two options:
data.json inside your vault, as most uploader plugins do. They travel with the vault, so setup happens once. Scope the R2 token to the one bucket, and keep data.json out of any git repo your vault lives in.data.json holds only the name of the entry. Turning this on clears the plaintext copy. The keychain is per-device and isn't synced, so you link the credentials again on each device — and if a device's secret store isn't working, uploads there fail until you link them or switch back to plaintext.Add rocketBANG/obsidian-image-attachment-upload as a beta plugin in BRAT.
Copy main.js and manifest.json into <vault>/.obsidian/plugins/image-attachment-upload/.
Requires NodeJS v22.13+ and pnpm 11.
pnpm i to install dependencies.pnpm run dev to start compilation in watch mode (produces main.js).pnpm run build for a production build.pnpm run lint to lint.pnpm version patch (or minor / major) after updating minAppVersion manually in manifest.json if needed — this bumps manifest.json, package.json, and versions.json.v prefix); the release workflow builds the plugin and drafts a GitHub release with main.js and manifest.json attached.