Org-capture Alternative in Common Lisp

2026-03-03

Here is the llink to the repo if you want to see it.

Link to video: Setup and Use

For a long time I have been using a small bookmarklet that talks to Emacs through org-protocol. It is one of those elegant little hacks that feel like cheating in the best possible way. I click a bookmarklet, the browser hands control to org-protocol://capture , and Emacs quietly drops the URL into my Org file using a predefined template. It is fast, local and it respects my workflow. More than that it feels native.

But it is also tightly coupled to one machine. If Emacs is not running, nothing happens. If I am on a different device, the capture path changes. If I want this to work from my phone or a remote browser session, I must recreate the same local plumbing. The whole system assumes that the browser and Emacs live together. That assumption is what I wanted to break.

I also wanted to build something useful with Common Lisp for a long time. So I built a small Common Lisp utility that runs as a daemon on my local machine and hopefully someday on my VPS. It does only one thing. It accepts an HTTP request and appends a structured entry into an Org file. That is all. No database. No framework ceremony. Just a small web server and a file append. The architecture is almost embarrassingly simple. There is a single binary running on the server. It uses Hunchentoot to listen on a port, usually bound to 127.0.0.1. I plant to use a reverse proxy such as nginx can sit in front if I want TLS later, but the core server is plain HTTP. The server exposes one endpoint, /capture. When it receives a POST request with fields like url, title, body , and a shared token, it writes a new Org heading into a file. It also writes a line into a log file with a timestamp and log level. The paths are built relative to the user’s home directory so the system remains portable and not hardcoded to one environment.

The logging is intentionally tiny. This is not some complex software that requires logging but I just wanted to taste how logging works. It just writes lines with a timestamp and severity, controlled by a configurable log level. The idea is not to build an observability platform but to have enough traceability when something fails. The server checks for a config.lisp file at startup so that ports, file locations, and the secret token can be overridden without touching the core code. This makes the binary relocatable and cleanly configurable.

On the client side, instead of using a bookmarklet that calls org-protocol , I moved to a browser extension. This change was forced partly by modern browser security policies. Many websites enforce strict Content Security Policies that block arbitrary fetch calls from bookmarklets, especially to non-HTTPS endpoints. An extension is not bound by the page’s CSP in the same way. It can collect the current tab’s URL, title, and selected text, and send it to my server without fighting the page’s restrictions. The extension does not need a user interface. It is just a toolbar button that packages context and sends it off. The flow now looks like this. I click the extension icon. The extension extracts the current page’s metadata. It sends a POST request to my server with a token. The server validates the token, writes an Org entry, logs the event, and returns a short confirmation. The extension does not even need to show a popup. The act of clicking becomes a silent and that is how I like it.

In many ways, this is not better than org-protocol. In fact, for a single-machine workflow, org-protocol is arguably superior. It is direct and integrated into Emacs. There is no network, no daemon, no binary to manage. It is frictionless. If all I want is to capture links while sitting at my main machine, nothing beats it. The Common Lisp daemon introduces more moving parts. There is a server process. There is a systemd unit. There is logging. There is “secret” token. There may be nginx in front of it. There is an extension. There are more surfaces where something can break. In terms of minimalism, the old solution wins. But this new system is expandable in a way that org-protocol is not and it is a way to flex my lisp muscles.

Because the capture endpoint is now a network service, it can accept input from anywhere. A Telegram bot could send links into it. A shell script could post URLs discovered during a crawl. A mobile app could push reading material into the same archive. Another machine could bulk-import bookmarks. The capture mechanism is no longer bound to Emacs running on one desktop. It becomes a small ingestion API. Once it is an API, it can grow. The server could auto-tag links based on domain. It could detect GitHub URLs and classify them differently from blog posts. It could trigger an asynchronous job to download a SingleFile HTML archive of the page and store it alongside the Org entry. It could push a git commit after each capture. It could store metadata in SQLite instead of flat files. It could expose a /health endpoint for monitoring. It could enforce rate limits. It could be replicated across nodes. The initial design is deliberately small, but the architectural shape allows expansion.

There is also something philosophically satisfying about writing this in Common Lisp and compiling it into a standalone binary. The final result is not a REPL toy. It is an executable. It runs without Quicklisp at runtime. It can be started by systemd. It behaves like a proper Unix daemon. It reads configuration, logs events, listens on a socket, and writes to files. The fact that it was written in Lisp is almost invisible from the outside, yet internally it retains the flexibility and clarity of a Lisp program. The system is not glamorous. It does not solve a global problem. It simply moves a small piece of personal infrastructure from a local Emacs integration to a networked service. But that shift changes the direction of growth. It transforms link capture from a local action into an ingestion layer which I plan to extend in near future. It decouples the producer from the consumer. The browser produces context. The server stores it. Emacs becomes just one of many possible readers.

In the end, the old org-protocol workflow is still there. It is still beautiful and I love using it. I am still thinking of making both my emacs and my other utilities cooperate. But this new utility is an experiment in taking a tiny habit and giving it a more general architecture. It may not be better in the narrow sense of simplicity. It is better in the sense that it can evolve. And sometimes that is enough reason to build it.