Skuld is a Python CLI for creating and managing systemd services and timers with a local registry.
It is designed to monitor only services that were created or explicitly adopted by Skuld.
Skuld is one of the three Norns in Norse mythology, alongside Urdr (the past) and Verdandi (the present). She is tied to the future: what is owed, what is bound to happen, what is next in the thread of events.
That symbolism maps directly to this project.
- A timer is a promise to the future.
- A scheduled service is an obligation waiting for its time.
- The registry is the ledger of what must still happen.
Skuld does not try to manage everything in your system. It watches only what you intentionally place in its care, then makes sure those future actions remain visible, repeatable, and accountable.
- Create
.serviceunits and optional.timerunits. - Persist managed service metadata in a local JSON registry.
- Start, stop, restart, execute-now, inspect status, and read logs via
journalctl. - Adopt existing
systemdservices into the Skuld registry. - Run
doctorchecks to detect registry/unit mismatches. - Backfill missing registry fields from systemd with
skuld sync. - Generate an equivalent
skuld createcommand from an existing managed service withskuld recreate. - Show CPU, memory, GPU memory, and listening ports in
skuld list.
- Linux with
systemd(systemctl+journalctl). - Python 3.9+.
sudoprivileges for unit installation/removal.
No external Python packages are required.
git clone git@github.com:rod-americo/skuld.git
cd skuld
chmod +x ./skuldRun from project root:
./skuld --helpOptional: place it on your PATH.
sudo ln -s "$(pwd)/skuld" /usr/local/bin/skuldYou can set SKULD_SUDO_PASSWORD in .env (or environment variables), but this is not recommended for production systems.
When present, Skuld runs sudo non-interactively.
Lookup order:
SKULD_SUDO_PASSWORDfrom process environmentSKULD_ENV_FILEpath (if set).envin current working directory.envnext to theskuldscript~/.local/share/skuld/.env
To force regular interactive sudo and ignore env/.env password:
skuld --no-env-sudo <command> ...Example .env:
SKULD_SUDO_PASSWORD=your_password_hereSkuld stores managed services in:
~/.local/share/skuld/services.json
Only services present in this file can be operated by:
execstartstoprestartstatuslogsremovedescribeeditsync --name <service>
On startup, Skuld normalizes this file automatically (canonical keys/order, pretty JSON, trailing newline, and valid unique IDs). This helps keep legacy or hand-edited registries consistent.
skuld create \
--name my-worker \
--exec "python /opt/app/worker.py" \
--working-dir /opt/app \
--restart on-failureWhen --user <name> is provided, Skuld writes User=<name> and also injects:
Environment="HOME=<user-home>"Environment="USER=<name>"Environment="LOGNAME=<name>"Environment="PATH=<user-home>/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
This avoids common runtime issues where services started by systemd miss user-scoped environment defaults and accidentally fall back to /root.
skuld create \
--name my-job \
--exec "python /opt/app/job.py" \
--schedule "*-*-* *:00/15:00" \
--timer-persistentSkuld passes --schedule directly to systemd OnCalendar.
Common patterns:
- Every 15 minutes:
*-*-* *:00/15:00 - Every hour at minute 05:
*-*-* *:05:00 - Every day at 02:30:
*-*-* 02:30:00 - Every Monday at 08:00:
Mon *-*-* 08:00:00 - First day of each month at 00:01:
*-*-01 00:01:00 - Specific date/time:
2026-03-15 14:00:00
Useful checks:
systemd-analyze calendar "*-*-* *:00/15:00"
systemctl list-timers --allIf you run skuld list and see values like *-*-02 00:01, that means:
- any year
- any month
- day 2
- at
00:01
So it runs monthly, on the 2nd day, at 00:01.
To run a service continuously, create it without --schedule.
This creates only a .service unit (no .timer).
skuld create \
--name my-daemon \
--exec "node /opt/app/server.js" \
--working-dir /opt/app \
--restart alwaysTypical daemon lifecycle:
skuld start --name my-daemon
skuld status --name my-daemon
skuld logs --name my-daemon --follow
skuld stop --name my-daemonFor timer jobs and daemons, action routing is automatic:
- Timer job (
.timerexists):start/stop/restartact on.timer. - Daemon (no timer):
start/stop/restartact on.service. - To run a timer job immediately, use
exec <timer-job>(starts.serviceonce). - To pause future scheduled runs, use
stop <timer-job>. - To resume scheduled runs, use
start <timer-job>.
skuld listskuld list output includes: id | name | kind | service | timer | schedule | cpu | memory | gpu | ports.
skuld exec --name my-job
skuld exec my-jobskuld start --name my-worker
skuld start my-worker
skuld start 2 4 5
skuld stop --name my-worker
skuld stop my-worker
skuld stop 2 4 5
skuld restart --name my-worker
skuld restart my-worker
skuld restart api-worker 7skuld logs --name my-worker --lines 200
skuld logs my-worker 200
skuld logs --name my-worker --follow
skuld logs --name my-job --timer --since "1 hour ago"
skuld logs 3 --plain
skuld logs 3 --output short-iso--plain uses journalctl -o cat (message only, no timestamp/host/process prefix).
skuld describe --name my-worker
skuld describe my-workerskuld recreate --name my-worker
skuld recreate my-worker
skuld recreate 3This prints an equivalent skuld create ... command based on current registry/systemd data.
skuld edit --name my-worker --exec "python /opt/app/new_worker.py"
skuld edit my-worker --exec "python /opt/app/new_worker.py"
skuld edit --name my-job --schedule "*-*-* 03:00:00"
skuld edit --name my-job --clear-scheduleskuld adopt --name existing-service
skuld adopt existing-serviceskuld doctorskuld sync
skuld sync --name my-worker
skuld sync my-workerskuld remove --name my-worker
skuld remove --name my-worker --purgeskuld --helpCONTRIBUTING.md: contribution workflow and expectationsCHANGELOG.md: notable changes
This project is licensed under the MIT License.
See the LICENSE file for details.