← Chestnut

User Guide

From first launch to full setup.

Setup

Getting started

Install

Download Chestnut.dmg, open it, and drag Chestnut into Applications. Or use Homebrew:

brew install --cask gapmiss/tap/chestnut

First launch (Gatekeeper)

Chestnut is free and open source but not notarized, so macOS will block the first launch. Pick whichever unblock method you prefer:

Accessibility permission

Global hotkeys need the Accessibility permission. macOS usually prompts on first use. If the hotkeys don't work, check System Settings → Privacy & Security → Accessibility and make sure Chestnut is listed and enabled.

Core

Features

Vault Hopper V

Opens a palette listing every vault Obsidian knows about. Type to filter, arrow keys to move, to open.

The palette also doubles as the courier's destination picker. When you drop a file onto Chestnut, the same list opens with the source vault excluded.

Quick Capture Space

A floating panel for jotting markdown. Pick a target vault, type your thought, hit and it's appended to today's daily note (or an inbox file if daily notes aren't configured).

See Capture settings for inbox path, date format, and folder options.

Note Courier

Drag a .md file (from Finder or another app) onto Chestnut to move or copy it into another vault. A destination picker appears; choose a vault and Chestnut delivers the note.

A single non-markdown file or folder routes to a matching plugin first. If no plugin matches, the courier handles it as a plain file copy. Drop two or more items at once and the whole drop is a delivery, plugins included.

Drag folders from Finder. Obsidian's file explorer hands over a folder as its name alone, with no path and no vault, so there is nothing for Chestnut to find and deliver. Chestnut says as much rather than acting on a guess. Notes and attachments dragged straight from Obsidian work normally, several at a time included.

Extend

Plugins

Overview

Plugins are shell scripts that teach Chestnut new tricks. They live in ~/.config/chestnut/plugins/<name>/, each containing a manifest.json and one or more scripts. Chestnut feeds them input (a URL, a file path, clipboard text) and they produce output (a note to save, text to capture, a notification).

Directory structure

~/.config/chestnut/plugins/
  bookmark/
    manifest.json
    save-bookmark.sh
  ocr/
    manifest.json
    ocr.py

manifest.json reference

FieldTypeDescription
apinumberAlways 1.
namestringDisplay name (shown in menus and the picker).
descriptionstringShort description (shown under the name in the Plugins menu).
scriptstringPath to the script, relative to the plugin folder. Must be executable (shebang + chmod +x).
acceptsstring[]Input types this plugin handles. See input types.
extensionsstring[]Optional. Narrows file matching to specific extensions (e.g. ["csv", "txt"]). Omit to accept all files.
outputstringOutput mode. See output modes.
timeoutnumberOptional. Max seconds before the script is killed. Default: 10, clamped to 1 to 300.

Input types

TypeWhen it matches
textPlain text on the clipboard.
urlA URL on the clipboard or dropped as a web location.
imageAn image on the clipboard or an image file drop. A pasted image is written to a temp file first, as PNG when the clipboard offers one.
fileA non-markdown file drop. Narrow with extensions.
pdfA PDF file drop, matched on the .pdf extension.
folderA folder drop.
anyWildcard: matches every input type above. Still narrowable with extensions.

Output modes

ModeWhat happens
capturePre-fills the Quick Capture panel with the script's stdout.
saveWrites stdout directly to the active vault. First line = filename, rest = content.
clipboardCopies stdout to the system clipboard.
notifyShows stdout in a speech bubble.
structuredExpects a JSON envelope on stdout for runtime control.

Environment variables

These are set before your script runs:

VariableValue
CHESTNUT_INPUT_TYPEThe matched input type (text, url, etc.).
CHESTNUT_FILE_PATHAbsolute path to the dropped file (empty for clipboard input).
CHESTNUT_SOURCE_APPBundle ID of the app that owns the clipboard (when available).
CHESTNUT_TIMESTAMPISO 8601 timestamp.
CHESTNUT_PLUGIN_DIRAbsolute path to this plugin's folder.
HOMEUser's home directory.
PATHIncludes /opt/homebrew/bin and /usr/local/bin.

Stdin

For text and url input types, the clipboard content is piped to the script's stdin. For other types, stdin is /dev/null; use CHESTNUT_FILE_PATH instead.

Structured JSON envelope

When a manifest sets "output": "structured", the script must print a JSON object to stdout:

{
  "action": "save",
  "content": "# My Note\n\nBody text here.",
  "filename": "My Note.md",
  "vault": "ask",
  "folder": "imported",
  "notify": "Saved to Work",
  "attachments": [
    { "source": "/tmp/image.png", "filename": "image.png" }
  ]
}
FieldRequiredDescription
actionyesOne of capture, save, clipboard, notify.
contentnoThe note body or text to output.
filenamenoFilename for save. Defaults to Untitled.md, so it is worth setting. Unlike the plain save output mode, the envelope's first line is not used as a title, and the content is not split.
vaultnoWhere to save. One of "pinned", "last" (the vault you captured to most recently), "ask" to show the vault picker, or the absolute path of a vault. A vault name will not match: Chestnut keys vaults by path, because names collide. Omitting the field behaves like "ask".
foldernoSubfolder within the vault.
notifynoSpeech bubble text shown after the action completes.
attachmentsnoArray of { source, filename } objects. Each source is an absolute path to a file. Copies go into the vault's attachment folder, meaning the attachmentFolderPath from that vault's Obsidian settings, or the vault root when it isn't set. Note that folder moves the note but not its attachments. With capture, a file is copied only if the submitted note refers to it by filename, so write a ![[filename]] link into content.

Dispatch flow

  1. Chestnut classifies the clipboard content (for C) or the dropped item.
  2. It finds all enabled plugins whose accepts and extensions match.
  3. If one matches, it runs. If several match, a picker lets you choose.
  4. If none match, the item falls through to the courier (for drops) or nothing happens (for paste).

Drag-and-drop routing

Hot-reload

Chestnut watches ~/.config/chestnut/plugins/ with FSEvents. Adding, editing, or removing a manifest takes effect immediately. No restart needed.

Enable/disable

Right-click Chestnut → Plugins. Each installed plugin is listed with a checkmark. Click to toggle. Disabled plugins are persisted in state.json as disabledPlugins.

Example: a bookmark saver

manifest.json
{
  "api": 1,
  "name": "Bookmark",
  "description": "Save a URL as a markdown note",
  "script": "./save-bookmark.sh",
  "accepts": ["url"],
  "output": "save"
}
save-bookmark.sh
#!/bin/bash
title="$CHESTNUT_TITLE"
url=$(cat)
echo "${title:-Untitled}.md"
echo "# ${title:-Untitled}"
echo ""
echo "[${url}](${url})"
echo ""
echo "Saved on $(date +%Y-%m-%d)."

Copy the URL of a page, press C, and a note is saved to your vault.

Settings

Configuration

Chestnut keeps two files in ~/Library/Application Support/Chestnut/.

config.json is yours: hotkeys, custom sprite themes, capture destination. Chestnut never writes to it, apart from creating it on first run. Hand-editable; changes take effect on next launch. Right-click the pet and pick Settings → Edit Configuration… to open it.

state.json is Chestnut's: window position, size, opacity, theme, notice duration, reduce motion, pinned vault, disabled plugins, last capture vault. Everything in it has a control in the right-click menu, so there's no reason to edit it by hand. Chestnut rewrites it every time you move the window.

Keeping them apart means editing config.json while Chestnut is running is safe. Nothing the app does can overwrite it.

Upgrading: an upgrade is just a new .app. Neither file is touched, so nothing resets. If a release ever moves a setting from one file to the other, that one value goes back to its default and the release notes say so; it's always something you can re-pick from the right-click menu. Keys Chestnut no longer reads are ignored rather than removed, so nothing you edited by hand is ever rewritten or thrown away. You can delete a stale key yourself, or leave it.

If either file can't be parsed, Chestnut moves it aside and starts with defaults. Existing backups are never overwritten: a second failure becomes config.json.bak.1, and so on, so you can always recover your settings.

Hotkeys

{
  "hotkeys": {
    "capture": "control+option+space",
    "hopper": "control+option+v",
    "paste": "control+option+c",
    "notice": "control+option+o",
    "menu": "control+option+m"
  }
}

menu opens the right-click menu at Chestnut without a right-click, so Size, Theme, Settings, Plugins, both Undo items and Quit are all reachable from the keyboard. Arrow keys move through the menu and Return picks an item. Chestnut has no Dock icon and no menu bar item, and an app like that isn't listed in Force Quit either, so this is the only way to reach those items, including Quit, without landing a click on the sprite itself.

Keys: az, 09, space, tab, return, escape, delete, f1f12. Modifiers: control/ctrl, option/alt, command/cmd, shift. Every binding needs at least one of control, option or command. shift can be added to those but doesn't count on its own, since a global hotkey takes the keystroke away from every other app: a binding of "space" would leave you without a space bar. Bindings without a modifier are ignored, and the reason is written to the log. Set a binding to "" or "none" to disable it.

Using VoiceOver? Control-Option is VoiceOver's own modifier, so it takes every default binding above before Chestnut sees it, menu included. Swap option for shift in all five and relaunch: "control+shift+space", "control+shift+v", "control+shift+c", "control+shift+o", "control+shift+m".

Custom themes

Four built-in themes: Obsidian Night (default), Classic Wood, Brushed Steel, and Sunbleached. Add your own:

{
  "customThemes": [
    {
      "id": "dracula",
      "title": "Dracula",
      "palette": {
        "s": "#44475A",
        "S": "#6272A4",
        "d": "#282A36",
        "m": "#BD93F9",
        "o": "#191A21"
      }
    }
  ]
}
RoleKeyDescription
ShellsMain body color.
HighlightSRivets, raised edges.
ShadowdRecessed areas.
TrimmMetal fittings.
OutlineoBorder pixels.

Optional roles: p/P (gem/gem glint), k (mouth), t (tongue), e (eye white), b (pupil), z (sleep pixels). These have shared defaults and can be overridden per-theme.

For single-color tweaks without a whole theme, petPalette overrides individual roles on top of the active theme:

{
  "petTheme": "classic-wood",
  "petPalette": { "m": "#C0C0C0" }
}

Capture settings

Captures land on a note in the selected vault. The target is resolved in order:

  1. Obsidian daily note, if the daily-notes core plugin is enabled. Uses the vault's configured format and folder.
  2. Chestnut daily note, if you set captureFormat. Same Moment.js token subset: YYYY, YY, MM, M, DD, D, [literal].
  3. Static inbox (Inbox.md at the vault root by default).
{
  "captureFormat": "YYYY-MM-DD",
  "captureFolder": "captures",
  "captureInboxName": "Inbox.md"
}

Notice duration

How long the speech bubble stays visible. Set it from the right-click menu → Settings → Notice Bubble, which lists 3, 5, 10, 20 and 30 seconds with the current one checked. The next bubble uses it: no relaunch. It's stored in state.json.

{
  "noticeDuration": 10
}

Disabled plugins

Plugins toggled off in the right-click menu are stored in state.json, not config.json. Use the menu rather than editing the file: it takes effect immediately, and it's the only place the two can't disagree.

{
  "disabledPlugins": ["ocr", "code-snippet"]
}

Shortcuts

Hotkey reference

ShortcutActionNotes
Space Quick Capture Toggle the capture panel.
V Vault Hopper Toggle the vault palette.
C Paste Classify the clipboard and dispatch to a matching plugin.
O Notice action Follow through on the active speech bubble (e.g. open the note in Obsidian). Only registered while a bubble is visible.
M Right-click menu Opens the menu at Chestnut, with arrow keys and . The only route to Size, Theme, Settings, Plugins, Undo and Quit that doesn't need a mouse.

All four are configurable in config.json.

Help

Troubleshooting

Gatekeeper blocks the app

See Getting started. The quickest fix is the xattr command.

Whether a later brew upgrade asks again varies by macOS version. If it does, the same fix applies.

Hotkeys don't work

Chestnut needs the Accessibility permission. Check System Settings → Privacy & Security → Accessibility. If Chestnut is listed but greyed out, remove it and re-add it (toggle the switch off then on). A restart of the app may be needed after granting the permission.

If VoiceOver is running, that is the more likely cause. Control-Option is VoiceOver's own modifier, the "VO key", so VoiceOver claims all five of Chestnut's default shortcuts before Chestnut ever sees them: Control-Option-V is speech verbosity, Control-Option-M is the menu bar. Rebind them to a Control-Shift set in config.json (see Hotkeys) and relaunch. Right-clicking Chestnut still opens the menu meanwhile, and the menu, the capture panel and the vault palettes all read correctly with VoiceOver.

An undo says it failed

Undo refuses when the file it needs is no longer where it was left, most often because the note was renamed, moved or edited in Obsidian since. The alert offers Keep and Discard Entry. Keep leaves the record in place, which means it comes up again on the next click and nothing older can be reached until it's dealt with. Discard Entry drops the record and touches no files: use it once you've decided you don't need that one reversed.

A delivery of several notes can also come back partly done. Undo tries every file rather than stopping at the first failure, so the ones it could reach are home and the alert names the ones that stayed put. That record is spent; a second try can only re-fail on the files already returned.

obsidian CLI not found

Chestnut looks for the obsidian CLI in /opt/homebrew/bin and /usr/local/bin only. It does not search $PATH. If you installed the CLI elsewhere, symlink it into one of those directories. The CLI is optional: every feature has a direct-filesystem fallback.

Corrupt config

If config.json can't be parsed, Chestnut renames it to config.json.bak and loads defaults. Your old settings are preserved in the backup, untouched. If a backup already exists it is kept, and the new one becomes config.json.bak.1. Fix the JSON syntax and rename it back.

File locations:

~/Library/Application Support/Chestnut/config.json   (yours)
~/Library/Application Support/Chestnut/state.json    (Chestnut's)