
Security News
GitHub Actions Checkout Now Blocks Risky pull_request_target Checkouts
GitHub Actions checkout now blocks risky pull_request_target checkouts by default to help prevent pwn request supply chain attacks.
hermitstash-sync
Advanced tools
Desktop sync client for HermitStash — post-quantum encrypted file sync
Desktop file sync client for HermitStash — post-quantum encrypted, self-hosted file sync.
AGPL-3.0 · Security Policy · Support on Ko-fi
HermitStash Sync is the companion to HermitStash — same author, same philosophy, same caveats. If you haven't read the note at the top of the main repo, the short version is: this is a personal project, built by someone who is not a cryptographer, and it hasn't been audited.
This client inherits its security posture from HermitStash and from Node.js's OpenSSL 3.5 — I'm not rolling my own TLS or inventing key exchanges. But a sync client introduces its own surface area (file watching, state tracking, daemon lifecycle), and those parts are entirely my own work. I've tried to be careful, but "tried to be careful" is not a substitute for a professional review.
If you're already running HermitStash and you want your files to show up on the other end without dragging them there yourself — this is for that. If you're evaluating it for something where reliability and security truly matter, please factor in that it's one person, spare time, and zero formal review.
— .CooCoo (@dotCooCoo)
Status: Personal project · Not audited · API may change · Use at your own risk
Watches a local folder and keeps it in sync with a HermitStash server:
All connections use PQC TLS with TLS 1.3 minimum and a three-tier hybrid group list (SecP384r1MLKEM1024 → X25519MLKEM768 → SecP256r1MLKEM768, Level 5 preferred) plus optional mTLS client certificates. Client certs auto-renew on startup when within 60 days of expiry — no admin action needed.
node:sqlite and OpenSSL 3.5+ PQC support)# From source
git clone https://github.com/dotCooCoo/hermitstash-sync.git
cd hermitstash-sync
# Or use pre-built binary (no Node.js required)
# Download from Releases for your platform
# Or run in Docker (see below)
docker pull ghcr.io/dotcoocoo/hermitstash-sync:latest
The image pulls the signed SEA binary from the matching GitHub Release during build, verifies its SHA3-512 checksum and P-384 ECDSA signature before installing, and runs non-root with a minimal Debian-slim base. Two volumes: /config holds persistent state (API key, mTLS certs, state DB, logs), /data is the sync folder.
# First run — enrolls and starts syncing
docker run -d \
--name hermitstash-sync \
-e HERMITSTASH_SERVER_URL=https://hermitstash.example.com \
-e HERMITSTASH_ENROLLMENT_CODE=HSTASH-XXXX-XXXX-XXXX \
-v hermitstash-sync-config:/config \
-v /path/on/host:/data \
--restart unless-stopped \
ghcr.io/dotcoocoo/hermitstash-sync:latest
Subsequent restarts skip enrollment — the API key and mTLS certs persist on the hermitstash-sync-config volume. A Compose file is available at docker-compose.example.yml.
Auto-update is always disabled inside the container (binary self-replace would violate the immutable-image model — pull a new image tag to upgrade). All other features (mTLS cert auto-renewal, PQC TLS, SHA3-512 checksums, sync bundle semantics) work identically to the native binary.
Testing: PRs touching Dockerfile, docker/, or scripts/verify-release.js trigger .github/workflows/docker-e2e.yml, which builds the image against the latest published release and runs packaging checks (OCI labels, non-root user, volumes, env defaults, entrypoint error paths). Full container-to-server e2e (enrollment, bidirectional sync, restart persistence, graceful shutdown) runs locally via node tests/run-all.js when Docker is available on the dev machine.
In addition to the Docker image, the repo ships reference configs for running the daemon natively or on common orchestration platforms. Each is self-contained and shows the sync-client-specific shape: outbound-only, two volumes (/config + /data), enrollment via env vars on first run.
| Platform | File | Notes |
|---|---|---|
| Unraid | unraid-template.xml | Community Apps template. Point the template URL at https://raw.githubusercontent.com/dotCooCoo/hermitstash-sync/main/unraid-template.xml to install. |
| systemd (native Linux) | deploy/install.sh + deploy/hermitstash-sync.service | `curl |
| Podman | deploy/podman.sh | Rootless by default (RHEL/Fedora/Alma/Rocky idiomatic). Also generates a user or system systemd unit for auto-restart. Image carries the io.containers.autoupdate=registry label so podman-auto-update.timer can refresh it. |
| Kubernetes | deploy/kubernetes.yml | Namespace + 2 PVCs + Deployment (replicas=1, strategy=Recreate) + Secret for enrollment. No Service — the client is outbound-only. runAsNonRoot, readOnlyRootFilesystem, dropped capabilities. |
For fleet deployment, use deploy/install.sh inside Ansible / SaltStack / your config-management tool of choice — it's idempotent and respects the standard VERSION, INSTALL_DIR, CONFIG_DIR, SYNC_DIR, SERVICE_USER env overrides.
Each deployment shape has its own update path. The in-binary self-replace only runs when the daemon has write access to its own executable — otherwise an external updater handles it.
| Deployment | Update path | Enabled by default |
|---|---|---|
| Bare binary (no systemd) | In-daemon: polls GitHub every 6h, verifies SHA3-512 + ECDSA, renames current → .prev, spawns new, 60s probation + auto-rollback on crash. | Yes — config "autoUpdate": true |
systemd (via install.sh) | External: hermitstash-sync-update.timer fires daily + 4h random delay; update.sh downloads + verifies + atomic swap + systemctl restart, rolls back if the daemon doesn't report RUNNING within 60s. In-daemon path is disabled (can't cross the root/hermit permission boundary under ProtectSystem=strict). | Opt-in — install with HERMITSTASH_AUTO_UPDATE=yes to enable the timer |
| Docker | Pull a new image tag manually (docker pull ... && docker restart ...). In-daemon self-replace is hard-disabled — mutating /usr/local/bin inside a running image violates the immutable-image model. | No |
| Podman | podman auto-update reads the image's io.containers.autoupdate=registry label and pulls the newest digest for the current tag. Enable podman-auto-update.timer (system or --user) to run on a schedule. | Opt-in via timer |
| Kubernetes | Bump the image tag in your manifest and kubectl apply. imagePullPolicy: IfNotPresent means you need to delete the pod or roll the Deployment to pick up a floating tag. Consider a pinned digest + a GitOps flow for production. | No |
# 1. Set up the connection
hermitstash-sync init
# 2. Start syncing (foreground)
hermitstash-sync start
# 3. Or run as a background daemon
hermitstash-sync start --daemon
# 4. Check status
hermitstash-sync status
# 5. Stop the daemon
hermitstash-sync stop
| Command | Description |
|---|---|
init | Interactive setup — enrollment code or API key, server URL, sync folder |
init --non-interactive | Headless enrollment from env vars (HERMITSTASH_SERVER_URL, HERMITSTASH_ENROLLMENT_CODE, HERMITSTASH_SYNC_FOLDER, HERMITSTASH_AUTO_UPDATE) — intended for Docker/CI |
start | Start sync in foreground |
start --daemon | Start sync as background daemon |
start --no-autoupdate | Start without polling GitHub Releases for new binaries |
status | Show sync status, file count, last sync time, errors |
stop | Stop the background daemon |
log | Show last 50 log lines |
log --follow, -f | Tail the log file in real-time |
resync | Force a full re-sync from scratch |
repair | Re-provision mTLS certificate using an admin-issued enrollment code (run this if your cert is revoked or the daemon can't connect after a certificate reissue) |
version | Show version and OpenSSL info |
--help, -h | Show usage information |
Config file: ~/.hermitstash-sync/config.json (or $HERMITSTASH_SYNC_CONFIG_DIR/config.json — useful for containers, where /config is a common mount point).
{
"server": "https://hermitstash.com",
"bundleId": "your-sync-bundle-id",
"shareId": "your-share-id",
"syncFolder": "/home/user/Documents/synced",
"mtls": {
"cert": "/path/to/client.crt",
"key": "/path/to/client.key",
"ca": "/path/to/ca.crt"
},
"ignore": ["*.log", "build/**"],
"logLevel": "info",
"autoUpdate": true
}
The following patterns are always excluded from sync:
| Pattern | Reason |
|---|---|
.DS_Store, .Spotlight-V100/**, .Trashes/** | macOS system files |
Thumbs.db, ehthumbs.db, desktop.ini | Windows system files |
.git/**, .svn/** | Version control |
node_modules/**, __pycache__/** | Package/build artifacts |
*.tmp, *.swp, *.swo, *~ | Editor temp files |
.hermitstash-sync/** | Client config directory |
Add custom patterns in:
config.json → ignore array.hermitstash-ignore file in the sync folder root (one pattern per line, # comments supported)Supported pattern syntax: exact filename (file.txt), extension (*.log), and directory recursion (build/**).
The API key is stored in your OS keychain:
secret-tool)Falls back to ~/.hermitstash-sync/credentials (permissions 0600) on headless systems.
Binary (SEA) installs poll GitHub Releases every 6 hours. When a newer version exists, the daemon downloads the binary for its platform, verifies the SHA3-512 checksum, and verifies a raw P-384 ECDSA signature over that digest using a public key embedded in the binary at build time. Only after both checks pass does it rename the current binary to .prev, write the new one in place, and spawn itself. If the new binary crashes within 60 seconds of first start, the next launch restores .prev.
Source installs (running from git clone) do not self-replace — the daemon logs a notice when a new version is out and expects you to git pull yourself.
Disable per-invocation with start --no-autoupdate, or globally by setting "autoUpdate": false in config.json.
The signing key is held only by the release pipeline; the daemon cannot install a binary signed by anything else. If no pubkey is embedded in this build, auto-update is disabled and logged at startup.
Windows binaries are not Authenticode-signed. The first time you run hermitstash-sync.exe, Windows Defender SmartScreen may show a "Windows protected your PC" dialog and require you to click More info → Run anyway. This is a reputation-based warning for unsigned executables; it goes away as more users download and run the same binary.
If you want to verify authenticity before running:
# SHA3-512 checksum
sha3sum -a 512 -c hermitstash-sync-vX.Y.Z-win-x64.exe.sha3-512
# GPG signature (import the public key once, then verify)
gpg --import gpg-public-key.asc
gpg --verify hermitstash-sync-vX.Y.Z-win-x64.exe.asc hermitstash-sync-vX.Y.Z-win-x64.exe
Both files are attached to every release. The GPG key fingerprint and the ECDSA auto-update pubkey are both baked into the binary itself, so once the first release is trusted, subsequent auto-updates verify against those keys without any further ceremony.
POST /sync/renew-cert to rotate it using the current API key. No admin action needed unless the cert has already been revoked (use repair for that).shareId configured, the client fetches the bundle manifest and downloads all existing files from the server, then uploads any local files not yet on the server. Existing local files are verified against server checksums using parallel SHA3-512 hashing across the worker thread pool.file_added, file_replaced, file_removed, file_renamed) and a file watcher detects local changes. Changes are debounced (500 ms) to avoid redundant uploads during active writes. All checksum computations are dispatched to the worker pool to keep the main thread responsive.The status command shows which state the daemon is in:
| State | Meaning |
|---|---|
DISCONNECTED | Not connected to server |
CONNECTING | Establishing WebSocket connection |
CATCHING_UP | Downloading changes missed while offline |
SYNCED | Fully synchronized, watching for changes |
UPLOADING | Actively uploading files |
DOWNLOADING | Actively downloading files |
RECONNECTING | Connection lost, waiting to retry |
ERROR | Something went wrong (check logs) |
SecP384r1MLKEM1024:X25519MLKEM768:SecP256r1MLKEM768 (NIST Level 5 preferred, Level 3 and Level 1 fallback for broad server compatibility). Both ecdhCurve and groups are set so Node negotiates the hybrid group even on older OpenSSL builds..tmp file, verify checksum, then rename.tmp files from interrupted downloads removed on startupLogs are written to ~/.hermitstash-sync/hermitstash-sync.log in JSON format — one object per line with ts, level, and msg fields.
Log levels: debug, info, warn, error.
The log file is rotated at 10 MB. The current log is renamed to .log.1 and a fresh file is started. Only one rotated copy is kept.
| macOS | Linux | Windows | |
|---|---|---|---|
| Keychain | Keychain Access | GNOME Keyring / KDE Wallet | Credential Manager |
| Daemon | start --daemon | start --daemon | start --daemon |
| Resync signal | SIGHUP | SIGHUP | Not supported — restart daemon |
| Auto-start | launchd | systemd | Task Scheduler |
# ~/.config/systemd/user/hermitstash-sync.service
[Unit]
Description=HermitStash Sync
After=network-online.target
[Service]
ExecStart=/usr/local/bin/hermitstash-sync start
Restart=on-failure
RestartSec=10
[Install]
WantedBy=default.target
systemctl --user enable hermitstash-sync
systemctl --user start hermitstash-sync
<!-- ~/Library/LaunchAgents/com.hermitstash.sync.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.hermitstash.sync</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/hermitstash-sync</string>
<string>start</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
</dict>
</plist>
bin/hermitstash-sync.js CLI entry point
lib/cli.js Command parser and dispatcher
lib/config.js Config file management
lib/constants.js All constants, message types, defaults
lib/checksum.js SHA3-512 hashing (single + worker pool)
lib/daemon.js Daemonization, PID file, signal handlers
lib/http-client.js HTTP client with PQC agent + ECIES
lib/keychain.js OS keychain for API key storage
lib/logger.js Structured JSON logger with rotation
lib/state-db.js Local SQLite state database (node:sqlite)
lib/sync-engine.js Core sync loop orchestrator
lib/watcher.js fs.watch with debounce and ignore patterns
lib/worker-pool.js Generic worker thread pool
lib/workers/checksum-worker.js SHA3-512 hashing worker thread
lib/ws-client.js Minimal WebSocket client with PQC TLS
The sync client ships as a standalone binary — no Node.js installation required on the target machine.
| Runtime | Node.js SEA binary (Node.js + app bundled into a single executable) |
| Build | GitHub Actions on tag push (v*) — automated via .github/workflows/release.yml |
| Platforms | Windows x64, Linux x64, Linux ARM64, macOS ARM64 (Intel Macs: use the ARM64 binary under Rosetta 2) |
| Artifacts | hermitstash-sync-vX.Y.Z-{win,linux,macos}-{x64,arm64}[.exe] + SHA3-512 checksum + GPG signature, per platform |
| Signing | GPG (P-384) for humans + raw P-384 ECDSA over SHA3-512 digest for the auto-update channel. No Authenticode — see Windows note below. |
| TLS | PQC hybrid: SecP384r1MLKEM1024 > X25519MLKEM768 > SecP256r1MLKEM768 (Level 5 preferred) |
| Dependencies | Zero npm runtime packages — all vendored |
git tag vX.Y.Z && git push origin vX.Y.Z
GitHub Actions automatically:
secrets.GPG_PRIVATE_KEY)Download the latest release from the Releases page.
# Requires Node.js 24+ and postject
node --experimental-sea-config build/sea-config.json
cp $(which node) build/hermitstash-sync
npx postject build/hermitstash-sync NODE_SEA_BLOB build/hermitstash-sync.blob \
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
Or use the local release script: bash scripts/release.sh (builds + signs + optional VirusTotal scan).
I want to be straightforward about this: I'm not currently accepting code contributions, and I want to explain why rather than just saying no.
HermitStash Sync is a security-focused project maintained by one person. Reviewing external code contributions to a cryptographic system is something I don't feel I can do responsibly right now — I'm still learning, and I'd rather not merge code I can't fully evaluate myself. Accepting PRs would mean either rubber-stamping changes I don't understand (bad) or asking contributors to wait indefinitely while I figure it out (also bad). The honest answer is that I'm not set up for it yet.
That said, there are a lot of ways to help that I genuinely welcome:
If you've built something on top of HermitStash, or you're running it somewhere interesting, I'd love to hear about that too — feel free to open an issue just to say hi.
This may change in the future. If HermitStash Sync grows to a point where I can responsibly review external code, I'll update this section. Until then: thank you for understanding, and thank you for being interested enough to consider contributing in the first place.
If you've read this far — thank you. Building and sharing HermitStash has been one of the most rewarding things I've worked on, and the fact that you took the time to look at it means a lot.
If HermitStash has been useful to you and you'd like to buy me a coffee, you can do so at ko-fi.com/dotcoocoo. It's never expected, always appreciated.
FAQs
Desktop sync client for HermitStash — post-quantum encrypted file sync
The npm package hermitstash-sync receives a total of 0 weekly downloads. As such, hermitstash-sync popularity was classified as not popular.
We found that hermitstash-sync demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
GitHub Actions checkout now blocks risky pull_request_target checkouts by default to help prevent pwn request supply chain attacks.

Product
Socket now supports Custom Roles and Repository Access Permissions so organizations can control who can access specific repositories and actions.

Product
Socket MCP now lets AI assistants review org alerts, investigate threats using the Socket threat feed, and inspect package files in addition to dependency scoring.