The tools are listed in the README. Their descriptions, which the agent reads, are the reference; this page explains how they fit together.
Every tool that takes a note_ref accepts either a note's internal ID or its
URL, such as https://hackmd.io/@owner/slug. If a URL matches no note, or more
than one, the tool does not guess: it returns the candidates so the agent can
pick.
Notes the tools return carry a note_url, https://hackmd.io/<id>, which opens
personal and team notes alike. It is a link, not an invite: whoever follows it
still needs read access. It is absent with a custom HACKMD_API_URL, whose
website the server cannot know, and on trashed notes, whose link answers 404.
Notes live in a workspace: your personal one, or a team's. Tools take a
team_path for a team and omit it for personal notes. hackmd_get_me lists the
teams you belong to.
hackmd_list_notes reads one of four lists through source:
source |
Lists |
|---|---|
workspace (default) |
notes in a personal or team workspace |
history |
the account's view history, in the order HackMD returns it |
trash |
trashed personal notes |
tracked |
local files synced by hackmd_pull_note, read without any request |
HackMD returns whole collections, so filtering, sorting, and paging happen in the server. No list searches note bodies.
The safe edit is a patch:
hackmd_get_notereturns the body, apatch_path, and abody_hash.- The agent sends
hackmd_update_notea patch in the*** Begin Patchformat, headed with that exactpatch_path, and passesbody_hashback asexpected_hash.
*** Begin Patch
*** Update File: notes/<id>.md
@@ ## Action items
- Ship the release notes
-- Fix the typo in the syllabus
+- Fix the typo in the syllabus (done)
*** End Patch
The patch is applied only if every hunk's context matches the current body
exactly once; ambiguous or missing context is an error, never a guess. Text
after @@ is an anchor line that narrows where the hunk applies. A patch whose
patch_path names a different note is refused, so an edit prepared for one note
cannot land on another. If expected_hash no longer matches, someone changed
the note since the agent read it, and the write is refused. The hash is
optional, and HackMD has no conditional write: the check runs just before the
write, so it catches every change made before then but not one landing in the
instant between the two.
content replaces the whole body instead. It is for rewriting a note from
scratch and carries the destructive hint; nothing this server offers can undo
it.
A metadata-only update (title, tags, description, permalink, permissions,
folder) still sends the body: HackMD is believed to blank a body the PATCH
omits, so the server reads the current one and sends it back. That moves the
whole body twice more than a metadata change used to: one download before the
write and one upload with it. An edit landing between that read and the write
is reverted; expected_hash works here too and catches any made before it. The
title field always takes effect. The body sets a title only when a note is
created without one (a front-matter title:, then the first H1, then
"Untitled"), and later body edits never change it, so hackmd_pull_note and
hackmd_push_note report title_drift when the body's own title no longer
matches the listed one. They report it only when they can read that title for
certain (plain front matter, or an H1 that is the body's first line of text),
and push only on a push or a no-op, so a missing title_drift does not mean the
titles match. Bodies are stored with \n line endings: \r\n and a
lone \r read back as \n, so every body you supply, and every local file read
for sync, is converted before it is written or hashed; a body the server reads
back and resends goes as it was read. A folder move is not waited on:
the result's folder_placement_confirmed says whether the read-back already
shows the note there (null when no folder was asked for).
No note PATCH is retried, even after a rate limit. Most carry a body read or
checked just before (a patch, a push, a metadata update, content with
expected_hash), and even a plain content replacement, which reads nothing
first, would land after the backoff over edits made in the meantime. The
error goes back to the agent, which reads again.
Body edits are read back until HackMD shows them, because some writes become
visible only after a delay. Folder updates and a new note's folder placement
are read back the same way; deletions, restores, folder creation, and image
uploads are reported as HackMD acknowledged them. A write that may or may not
have landed (the connection dropped, HackMD answered 5xx, or it answered
success with a reply that could not be read) is reported as unconfirmed with
kind readback, and the agent is told to look rather than retry. Image uploads
are the exception: nothing can look for an uploaded image, and a second upload
only leaves an unused copy, so an unreadable reply there is upstream.
Sync turns a note into a Markdown file that any editor, script, or git repository can work with:
-
hackmd_pull_notewrites the note to an absolute.mdpath and records a baseline: the exact body as it was at pull time. It returns that body'sbody_hash, in the same formhackmd_get_noteand a successful push report. When it replaced a file it could read,changesis a bounded diff of what the pull changed in it;title_driftnames the title the body gives when the listed one differs (see above). -
You or the agent edit the file locally.
-
hackmd_get_notewithlocal_pathreports where things stand by comparing the file, the baseline, and the remote note:State Meaning in_syncnothing to do local_changedonly the file changed; push it remote_changedonly the note changed; pull with overwrite_local: trueconflictboth changed -
hackmd_push_notewrites the file back. In the defaultsafestrategy it re-reads the note right before writing, and if the note changed since the pull it writes nothing.
On a conflict, push returns a short diff, saves the current remote body next to
your file as <name>.remote.md, and reports its remote_body_hash. Merge the
two, then push again with expected_remote_hash set to that hash: the push
goes through only if the remote still has exactly that body. A .remote.md you
edited yourself is never overwritten by a later conflict.
strategy: overwrite with confirm: true replaces the remote regardless. A
pull over any existing file needs overwrite_local: true. Even then, a pull
over a tracked file with unpushed edits is refused unless
discard_local_changes: true is given, and so is a pull over a file with no
usable sync record (none, or a broken one) whose content differs from the
note, such as one fetched by other means and edited since: nothing shows
those edits were ever pushed. To keep them, pull to another path, carry the
edits into that file, and push it. A file that already matches the note is
adopted, and tracked from then on. hackmd_untrack_note, given the note_id
and confirm: true, forgets the sync record without touching the file or the
note; a later pull over that file, once edited, needs discard_local_changes
like any other unrecorded file.
hackmd_update_folderupdates team folder metadata and sets folder order withchild_order, which may name only the folder's own children. The read-back confirms HackMD stored the order; the team order route is inferred from the personal one and unmeasured. HackMD reports folder moves as successful while doing nothing, so moves are refused outright. Personal folder metadata updates are refused too, since nothing confirms they take effect.hackmd_delete_folderleaves a folder that has child folders alone unlessconfirm: trueis given. HackMD does not report which notes a folder holds, so checkfolder_idsfromhackmd_get_notefirst if that matters.hackmd_delete_notewithrestore: truebrings a personal note back from trash. Team notes cannot be restored through the API.hackmd_upload_note_imageuploads an image, from a local file or a public URL, to a personal or team note and returns its CDN link. The link is public whenever the note is guest-readable; an anonymous fetch of an image on an owner-only note was refused when measured, but treat that as observed, not promised. So the result also reportspublicly_readable: right after the upload the server sends one signed-outHEADfor the link, without the token and only to HackMD's own site, and reportstrueif an image or a redirect to a presigned storage URL comes back,falseif the request is refused, andnullif the check could not run or proved nothing (any other redirect, such as to a login page, counts as nothing). It is alwaysnullwhen the API is neither on the site's own host nor on itsapi.subdomain. Afalseusually means the note is not guest-readable, and signed-out readers will not see the image until it is. A localimage_pathneeds a workspace root (see configuration.md). Instead of a local file,image_urlre-hosts an image from a public URL, which is how a note's imgur links move to HackMD without a download step. The server fetches it only overhttpson the default port, only from a host whose every resolved address is public (loopback, private, link-local, CGNAT and reserved ranges are refused, and the checked addresses are pinned for the connection), and re-checks each of at most 5 redirects the same way. An address check cannot see translation beyond the server: on an IPv6-only network whose NAT64 gateway uses its own prefix, a private IPv4 address can arrive looking public, so such a network must filter that at its egress. Nor can it see whom a public host serves: one that answers only your network, by source address, passes, and its image is republished like any other. Arate_limitedorupstreamerror from animage_urlfetch is about the image host, not HackMD, and so is anetworkerror naming a host that does not resolve (the server cannot tell a missing host from DNS being down). The size an image host declares is checked before the download, and withoutconfirm_large_filean undeclared one stops at 5 MiB. A URL upload is named after the URL's last path segment, with the extension of the type its bytes show. Either source must be a PNG, JPEG, GIF, or WebP by its leading bytes; files over 5 MiB needconfirm_large_file: trueand files over 10 MiB are refused.
A failed tool call carries a human-readable message that names the fix and a
stable _meta.error_kind that a client can branch on. Retries the server made
on its own are reported in _meta.retry.