twocents/open source

Operations and security

Daemon health, macOS startup, and private local data.

The default runtime home is ~/.guildscribe. --home <absolute-path> selects another home for every command.

~/.guildscribe/
  .env
  guildscribe.sqlite
  guildscribe.lock
  runtime.json
  logs/
    daemon.jsonl
    daemon.jsonl.1
    launchd.stdout.log
    launchd.stderr.log
  in-progress/
    <recordingID>/
      participants.json
      intervals/
        <intervalID>.json
        <intervalID>.pcm
      audio.part.mp3
  recordings/
    <year>/
      <month>/
        <recordingID>/
          participants.json
          intervals/
            <intervalID>.json
            <intervalID>.pcm
          audio.mp3

guildscribe.lock exists only while the daemon is running and contains a versioned JSON record with its process ID and start time. runtime.json is an owner-only atomic heartbeat containing daemon phase, Discord readiness, update time, and the last sanitized runtime error. The daemon refreshes it every 10 seconds; a live lock with no matching heartbeat within 30 seconds is stale. Completed raw PCM files and their JSON interval sidecars remain permanently with the published recording so speaker audio, identity, timing, and capture gaps stay inspectable and available to optional processing.

The runtime home uses mode 0700 and .env uses mode 0600 on POSIX systems. Guildscribe rejects a runtime home or configuration file that is a symbolic link, is owned by another user, has group/world permissions, or is not the expected filesystem type. Runtime files never belong in the source checkout.

daemon.jsonl is the structured operational log. Guildscribe rotates it at 10 MB and retains three prior files. The launchd stdout and stderr files capture wrapper failures and unhandled output before structured logging is available.

Status

guildscribe status reads saved state and reports daemon liveness, uptime, heartbeat time, Discord readiness, last sanitized runtime error, and one health classification: stopped, starting, ready, recording, degraded, stopping, or stale. It also reports the configured channel allowlist, whether transcription is enabled, the active recording, the latest published recording, and aggregate storage totals. The empty-channel grace timer is kept in memory and is not included in this report. It never contacts Discord, returns the token, or mutates state. A malformed lock is not proof that a daemon exists; a live lock without a current, PID-matched heartbeat is stale.

macOS startup

scripts/installLaunchAgent.sh builds Guildscribe and invokes guildscribe startup install. Installation requires an initialized runtime home and writes ~/Library/LaunchAgents/dev.guildscribe.daemon.plist with mode 0600. The plist contains no Discord token. It launches /bin/bash scripts/daemon, the selected runtime home, and the exact Node executable used for installation.

scripts/daemon sets the system and Homebrew PATH before executing guildscribe daemon run. See the contributor guide for the modules that implement startup.

The agent uses RunAtLoad, a 30-second launchd throttle, and a KeepAlive condition that restarts only after an unsuccessful exit. A crash is therefore restarted, while startup stop sends SIGTERM and the daemon's clean exit keeps it stopped. startup start or startup restart resumes it. Process type is Standard because live voice capture should not be background-throttled.

startup uninstall unloads the agent and removes only its plist. It never removes .env, recordings, in-progress recovery data, or logs. The scripts must run as the logged-in user and reject root execution.

Protect the runtime home

Keep tokens and private requests out of source control, command-line arguments, and logs. config show redacts secrets. Recordings, transcripts, participant metadata, messages, exports, and backups can contain private content. History commands return actual saved text; avoid dumping it into public logs.

Only explicitly configured slash interactions can execute user action hooks. Ordinary messages and context-menu interactions cannot. Treat executable allowlists and Discord integration permissions as security boundaries.

Upgrade and migration

Before upgrading, use database backup and preserve media and participant/interval files separately. Do not copy only a live SQLite main file: committed data can remain in its WAL.

On startup the daemon validates legacy recording/transcript documents, writes recovery bundles, imports SQLite state, and removes obsolete metadata only after successful import. Keep legacy-storage-backup-*.json bundles. Audio and interval/ participant files remain. Existing hooks must use the database references rather than expect recording.json. Building alone does not migrate a live home.

Linux capture is supported, but built-in Linux service management is not yet implemented. Choose your own supervisor there. Installing or starting a service is an explicit operational step, separate from building the package.

On this page