- Save location
- Add attachment
- Rename attachment
- Delete attachment
- Hook after adding attachments
- Paste from clipboard path
- Open
- Options
- See also
Save location
Option for attachment location is opts.attachments.folder
- for vault root, set it to
/. - for fixed folder, set it to
/folder-name. - for same folder as current file, set it to
./ - for sub folder in current folder, set it to
./folder-name
Add attachment
There are two attachment entry points:
require("obsidian.attachment").add(source, opts)is the lower-level API. It copies or downloadssource, then optionally inserts a link.require("obsidian.actions").add_attachment(source, opts)is the interactive action wrapper, and is what the code action invokes. It prompts whensourceis missing, opens a picker whensourceis a directory, then delegates toattachment.add().
For attachment.add(source, opts):
- If
sourceis a local file (orfile://URI), the file is copied. - If
sourceis ahttp(s)URL, the file is downloaded withcurl. - The destination path is resolved by
attachment.destination_path()and controlled by Save location.api.resolve_attachment_path()remains a compatibility alias. - Set
opts.new_nameto copy/download the attachment with a different destination basename. - Inserted links follow
opts.link.styleandopts.link.format; relative links omit an unnecessary./prefix.
For actions.add_attachment(source, opts):
- If
sourceis a local directory, a file picker is opened, and the selected file is copied. - If
sourceis missing or empty, obsidian.nvim prompts for a URL or file path. - The target
bufnrmust be an obsidian buffer.
Both functions accept the same opts table:
---@class obsidian.AddAttachmentOpts
---@field insert? boolean Insert the generated attachment link after adding. Defaults to true.
---@field bufnr? integer Buffer used for relative attachment resolution and link insertion. Defaults to current buffer.
---@field new_name? string Destination attachment basename. Path separators are rejected.
---@field position? obsidian.AttachmentPosition|integer[] Exact position where the link should be inserted.
---@field scope? string Context passed to callbacks as `ctx.scope`.Rename attachment
require("obsidian.attachment").rename(reference, new_name, opts, callback) resolves an existing attachment, renames it, and updates wiki and Markdown references that resolve to it. If new_name has no extension, the existing extension is retained.
Set opts.apply = false to receive the generated workspace edit without applying it. A callback is required in that mode.
Delete attachment
require("obsidian.attachment").delete(reference, opts, callback) resolves and permanently deletes an existing attachment. Ambiguous basename references are rejected; use a relative or vault-relative path to identify a specific duplicate.
Both operations resolve basename references from the configured attachment destination first, then search the vault. Their callbacks receive an error as the first argument.
Hook after adding attachments
Use callbacks.add_attachment or the ObsidianAttachmentAdded user autocmd:
require("obsidian").setup {
callbacks = {
add_attachment = function(path, ctx)
-- path: full path to the attached file in the vault
-- ctx.scope: context where the attachment was added
-- ctx.buffer: target buffer
end,
},
}
vim.api.nvim_create_autocmd("User", {
pattern = "ObsidianAttachmentAdded",
callback = function(ev)
local path = ev.data.path
local ctx = ev.data.ctx
end,
})Paste from clipboard path
If your clipboard contains a file path, you can add it directly with attachment.add().
The example below checks that the clipboard resolves to an existing path first and reports the aborted branch through obsidian.log:
local log = require "obsidian.log"
local paste_from_path = function()
local path = vim.trim(vim.fn.getreg "+")
if path == "" then
return log.warn "Clipboard is empty"
end
local stat_path = vim.startswith(path, "file://") and vim.uri_to_fname(path) or path
if not vim.uv.fs_stat(stat_path) then
return log.warn("Clipboard does not contain a valid path: %s", path)
end
vim.schedule(function()
require("obsidian.attachment").add(path, { insert = true })
end)
endTo customize how attachment is resolved, use opts.resolvers.attachment.
For example, pick with a terminal file manager:
require("obsidian").setup {
resolvers = {
attachment = function(ctx, done)
local tmp = vim.fn.tempname()
local buf = vim.api.nvim_create_buf(false, true)
local width = math.floor(vim.o.columns * 0.8)
local height = math.floor(vim.o.lines * 0.8)
local win = vim.api.nvim_open_win(buf, true, {
relative = "editor",
row = math.floor((vim.o.lines - height) / 2),
col = math.floor((vim.o.columns - width) / 2),
width = width,
height = height,
style = "minimal",
border = "rounded",
})
vim.fn.jobstart({ "yazi", "--chooser-file=" .. tmp }, {
term = true,
on_exit = function()
vim.api.nvim_win_close(win, true)
vim.api.nvim_buf_delete(buf, { force = true })
if vim.uv.fs_stat(tmp) then
local lines = vim.fn.readfile(tmp)
if lines[1] then
done { path = lines[1] }
return
end
end
done(nil)
end,
})
vim.cmd "startinsert"
end,
},
}See Resolvers for the full resolver contract.
Open
Attachment opening is by default controlled by :h vim.ui.open, customize it like following:
vim.ui.open = (function(overridden)
return function(uri, opt)
if vim.endswith(uri, ".png") then
vim.cmd("edit " .. uri) -- early return to just open in neovim
return
elseif vim.endswith(uri, ".pdf") then
opt = { cmd = { "zathura" } } -- override open app
end
return overridden(uri, opt)
end
end)(vim.ui.open)Put any where in you config that loads before you open attachments, a good place could be opts.callback.enter_note
Options
---@class obsidian.config.AttachmentsOpts
---
---Default folder to save images to, relative to the vault root (/) or current dir (.), see https://obsidian-nvim.github.io/obsidian.nvim/Attachment#options
---@field folder? string
---
---Default name for pasted images
---@field img_name_func? fun(): string
---
---Default text to insert for pasted images
---@field img_text_func? fun(path: obsidian.Path): string
---
---Whether to confirm the paste or not. Defaults to true.
---@field confirm_img_paste? boolean
attachments = {
folder = "attachments",
img_text_func = require("obsidian.builtin").img_text_func,
img_name_func = function()
return string.format("Pasted image %s", os.date "%Y%m%d%H%M%S")
end,
confirm_img_paste = true, -- TODO: move to paste module, paste.confirm
}See also
Images · Audio-recorder · File