A native macOS desktop companion for Obsidian users. gapmiss.github.io/chestnut
Chestnut is an always-on-top pixel-art treasure-chest creature that reacts to your writing activity and acts as a control surface across all your vaults. It watches the filesystem directly, no Obsidian plugin required, no settings modified.
Native Swift and SpriteKit: ~1.2 MB download, ~2 MB installed, ~3% idle CPU. No Electron, no bundled browser, no network calls.
Important
Project status: Chestnut is pre-1.0 and moving quickly. Expect frequent releases.
- Vault Hopper lists all your registered vaults. ⏎ opens, ⌘⏎ goes to today's daily note, ⌥⏎ reveals in Finder.
- Pin a vault so it always sorts first in every list and starts pre-selected for captures and deliveries. Toggle with the pin icon or ⌘P.
- Note Courier lets you drag files onto the pet and pick a destination
vault. Notes land at the vault root with their embedded attachments
(
![[…]],) resolved and carried along, references rewritten to match the destination layout. Other file types go to the attachment folder. Conflict-safe naming, journaled for undo. A plugin never takes a dropped file away from the courier: when both could handle it, Chestnut asks, with the plugin pre-selected and "Deliver to a vault" one arrow key away. Drag folders from Finder: Obsidian's file explorer drags a folder as its name alone, without the path, so Chestnut has nothing to deliver and says so instead. Notes and attachments dragged from Obsidian are fine, including several at once. - Quick Capture is a floating panel for jotting markdown into any vault. Formatting toolbar, ⌘B/⌘I/⌘K shortcuts, ⌘1-⌘9 to pick the vault. Drafts survive dismiss/reopen.
- Plugins let you extend drag-and-drop and a paste hotkey with shell
scripts. See the User Guide
for the full reference, PLUGINS.md for architecture details,
and
Examples/plugins/for ready-made examples. - After a capture or delivery, a speech bubble tells you where your text
went. Click it to open the note in Obsidian (uses the
obsidianCLI when available, falls back to opening the vault). - Idle, peek, writing, chomp, carry, deliver, sleep animations. Hand-coded pixel-art frames with swappable color themes.
brew install --cask gapmiss/tap/chestnutDownload Chestnut.dmg from the latest release, open it, and drag
Chestnut.app into Applications.
Chestnut is ad-hoc signed (not notarized), so macOS may block the first launch. To allow it:
- macOS 15+: System Settings → Privacy & Security, then Open Anyway, or
- macOS 14: right-click the app, then Open, then click Open in the dialog, or
- Remove the quarantine flag:
xattr -dr com.apple.quarantine /Applications/Chestnut.appWhether a later brew upgrade asks again varies by macOS version. If it
does, the same fix applies.
To start automatically, right-click the pet and toggle Settings → Launch at Login.
If you turned on Settings → Launch at Login, switch it off before you quit.
That deregisters the login item, which deleting the app does not. Then quit
Chestnut (right-click → Quit, ⌃⌥M → Quit, or pkill -x Chestnut) and remove
the app:
brew uninstall --cask chestnut # installed with Homebrew
rm -rf /Applications/Chestnut.app # installed manuallyChestnut writes to four places outside its own bundle, and nowhere else:
| Path | Contents |
|---|---|
~/Library/Application Support/Chestnut/ |
config.json, state.json, the undo journals journal.jsonl and captures.jsonl, and any backups beside them (.bak, .bak.1, … from a settings file that failed to parse, or a .pre-0.3 left by an old build) |
~/.config/chestnut/plugins/ |
Installed plugins. Created at every launch, so it exists even if you never wrote one |
~/Library/Logs/Chestnut/ |
chestnut.log and chestnut.log.1, written only while debug is on in config.json |
$TMPDIR/chestnut-plugins/ |
Scratch copies of pasted images on their way to a plugin. Swept at every launch, and macOS clears it anyway |
rm -rf ~/Library/Application\ Support/Chestnut \
~/.config/chestnut \
~/Library/Logs/ChestnutNone of those paths holds a vault or a note, so removing them leaves your writing where it is.
Worth knowing whether or not you are uninstalling: the undo journals hold
your note text. captures.jsonl keeps the exact text of each capture, and
journal.jsonl keeps a note's pre-delivery text whenever a delivery rewrote
its links, because that is what Undo puts back. Both are capped at 20 records
or 1 MB, so older text rolls off as you keep working, and neither is ever sent
anywhere.
- macOS 14+
- Swift 6 toolchain (Xcode or Command Line Tools), only needed to build from source
- No Xcode project; builds with SPM + Make
make build # swift build (CONFIG=debug|release)
make bundle # build → .build/Chestnut.app (ad-hoc codesign)
make run # bundle + open the app
make dmg # release build → .build/Chestnut.dmg
make check # runtime checks (no XCTest dependency)
make cleanQuit with right-click → Quit, ⌃⌥M → Quit, or pkill -x Chestnut.
Single Swift executable bundled into a .app by the Makefile.
| Layer | Technology | Role |
|---|---|---|
| Windows | AppKit | Borderless, transparent, always-on-top pet window |
| Pet rendering | SpriteKit | Sprite animation from hand-coded frame matrices |
| Panels | SwiftUI | Vault palette, capture panel |
Sources/Chestnut/
main.swift, AppDelegate.swift
Pet/ # PetWindow, PetScene, Sprites, PetController
Vaults/ # VaultRegistry, VaultWatcher
Actions/ # ObsidianBridge, Courier, Capture
Panels/ # SwiftUI palettes/panels (NSPanel-hosted)
Plugins/ # PluginManifest, PluginRegistry, PluginRunner, PluginDispatch, DropRouter, PluginPalette
Support/ # Config, Hotkeys, Journal
Two files in ~/Library/Application Support/Chestnut/:
config.jsonis yours: hotkeys, capture destination, custom sprite themes. Chestnut never writes to it, apart from creating it on first run. Hand-editable; changes take effect on next launch. Right-click → Settings → Edit Configuration… opens it.state.jsonis Chestnut's: window position, size, opacity, theme, notice duration, pinned vault, disabled plugins. Everything in it has a control in the right-click menu.
VoiceOver claims Chestnut's default shortcuts. Control-Option is VoiceOver's own modifier (the "VO key"), so with VoiceOver running ⌃⌥V is speech verbosity, ⌃⌥M is the menu bar, and none of Chestnut's five hotkeys reach Chestnut. That includes ⌃⌥M, which is otherwise the keyboard route to the right-click menu.
Rebind them in config.json to a prefix VoiceOver doesn't use, and relaunch:
{
"hotkeys": {
"capture": "control+shift+space",
"hopper": "control+shift+v",
"paste": "control+shift+c",
"notice": "control+shift+o",
"menu": "control+shift+m"
}
}Until then, the menu is still reachable by right-clicking Chestnut itself (the click has to land on the sprite, not the clear space around it). Once open, the menu, the Quick Capture panel and the vault palettes all read correctly, and arrowing through a palette announces each vault with its path.
Keeping them apart means editing config.json while Chestnut is running is
safe. Upgrading 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 menu. See the
User Guide
for the full reference.
- Read-only. Never modifies Obsidian's files or
.obsidian/settings. - Vaults keyed by path, not name (names collide).
- No network calls, no telemetry.
- The
obsidianCLI is optional. Every CLI call has a filesystem fallback. - No image assets. Sprites are string matrices mapped through a palette.
See CONTRIBUTING.md for build instructions and ground rules.
