A statically compiled Emacs init (PID 1) patchset, Emacs Lisp-based service supervisor and core component of Emacs-OS.
el-init is an Emacs Lisp-based, systemd-inspired init and
service-management solution. Whether you are building a full-send Emacs-OS, or
simply want to orchestrate your desktop session services from within EXWM,
or keep project daemons running while you work,
el-init has you covered. Use M-x elinit for the interactive
dashboard, or elinitctl from the terminal.
- Optional PID1 patchset – static build instruction files (Arch
PKGBUILD and Nix) and Emacs patches adding zombie reaping and
PID1 lifecycle hooks (
static-builds/). - Dependency graph – five relationship keywords give fine-grained control
over startup order and runtime interaction:
:after/:before– pure ordering (start A after B, or B before A).:requires– ordering plus hard dependency (if the required unit fails, the dependent is not started).:wants– ordering plus soft dependency (best-effort, failure tolerated).:conflicts– mutual exclusion (starting one unit stops the other, and vice versa).
Topological sort with cycle detection. Targets (
default.target,multi-user.target,graphical.target) group services into activation stages, just like systemd. - Event-driven process monitoring – uses Emacs’s built-in
set-process-sentinelfor zero-overhead exit detection. No polling. The hot path is a sentinel lambda that does a few hash lookups and optionally schedules arun-at-timerestart. - Crash loop detection – configurable restart window (default: 3 restarts
within 60 seconds marks a service as FAILED). Per-unit
:restart-secfor individual restart delays. - Lifecycle FSM – proper state machine with validated transitions: pending -> waiting-on-deps -> delayed -> started -> failed-to-spawn.
- Structured event system – hook-based events for process-exit,
process-started, target-reached, target-degraded, timer-trigger, and more.
Full programmatic integration via
elinit-event-hook. - Service types – long-running
simpledaemons with restart policies and run-onceoneshottasks with configurable blocking and timeout. - Graceful shutdown – SIGTERM with configurable timeout, then SIGKILL. Sentinel-driven completion tracking (no polling).
- 3-tier authority model –
/usr/lib/elinit.el/(vendor),/etc/elinit.el/(admin),~/.config/elinit.el/(user). Higher tiers override lower tiers completely. Daemon-reload support. - Persistent overrides – enable/disable, mask/unmask, restart-policy, and logging overrides persist across restarts.
- Timers – calendar and relative timers for oneshot, simple, and target units with persistent catch-up semantics after downtime.
- Dashboard –
M-x elinitfor a live view with lifecycle actions, policy changes, dependency inspection, and log viewing. - CLI –
sbin/elinitctlfor scripting and remote use (human and JSON output). - Logging – external log writer helpers with rotation and pruning for bounded growth (configurable per-file and total size caps). Optional structured log formats (human-readable text or compact binary with nanosecond timestamps) for machine-parseable records.
- Resource limits – per-service ulimit-style limits (
:limit-nofile,:limit-nproc,:limit-core,:limit-fsize,:limit-as) via a small C helper that callssetrlimit(2)before exec. - Sandbox – optional bubblewrap integration with curated profiles for process isolation (Linux only).
- systemd import –
sbin/elinit-importconverts systemd.servicefiles to elinit unit plists. - Comprehensive test suite – ~33,000 lines of tests across Elisp ERT, C (acutest), and POSIX shell frameworks.
- Emacs 28.1 or later
- For CLI:
emacsclient,base64, andsedin yourPATH - For bundled
libexechelpers: a C compiler inPATH(typicallycc,clang, orgcc)
See RETROSPECTIVE.md for the full disclosure and the human author’s unfiltered opinions on AI-driven development. Some of those opinions may surprise you.
This document is the complete reference. Run M-x elinit-handbook to open
it in Emacs at any time.
From Lisp Machines to GNU Emacs provides historical context. The Quick Start below gets you running immediately. The rest of the handbook provides detailed reference material:
- Service Definition - all unit-file keywords for defining services
- Unit Files - authority roots, file format, precedence, and editing workflow
- Startup and Lifecycle Model - DAG startup, lifecycle state, restart behavior
- Runtime Overrides and Reconciliation - policy overrides and reconcile semantics
- Target System - built-in targets, aliases, runlevel compatibility
- Timer Subsystem - triggers, retry/catch-up behavior, scheduler lifecycle
- Dashboard - service-first UI, keymap, inspect/actions
- CLI - transport wrapper, commands, output contracts, exit codes
- Events and Hooks - event payloads and integration hooks
- Persistence and Files - logs, rotation/prune, overrides and timer state files
- Customization Reference - all options and dashboard faces
- Command Reference - interactive and dashboard command map
- Security - manager targeting, transport boundary, trust gate, sandbox, rlimits
- PID1 - for the init hackers, tinkerers and adventurers among us - all docs for this crazy idea are in
./static-builds - Service Management Systems Comparison - matrix, conversions, import tooling
- Development - test/lint workflows
- Topical Index - exhaustive cross-reference
In 1958 John McCarthy sat down to design a language. He based it on lambda calculus. He called it Lisp. The paper came out in 1960. It was called “Recursive Functions of Symbolic Expressions and Their Computation by Machine.” The core idea was simple. Lists were data. Lists were also code. The language could rewrite itself.
FORTRAN crunched numbers. COBOL kept records. Lisp did whatever you told it to. Its primitives were so small you could rebuild the whole language from seven of them. McCarthy even wrote a Lisp interpreter in Lisp. He put it in the paper almost as an afterthought. It was the first time a language could run itself.
At the MIT AI Lab in the 1960s and 1970s, people took that idea and ran with it. They did not just write programs in Lisp. They built whole machines out of it. The Lisp Machine was hardware designed from the microcode up to run Lisp natively. Symbolics built them. Lisp Machines Inc. built them. Texas Instruments built them.
On a Symbolics 3600, everything was Lisp. The editor. The debugger. The window system. The file system. The operating system itself. You could click on any part of the running machine, read the source, change it, and watch the change take effect. No reboot. No recompile. No permission needed. There was no line between using the machine and programming it. That was the whole point.
The hackers at the AI Lab shared everything. Code lived on the machines and anyone could read it. Anyone could fix it. If you found a bug you just patched it. People walked down the hall and talked about what they changed. Sometimes they carried tapes. Later they carried floppies. The code moved freely because nobody thought to stop it.
Then the money showed up. In the early 1980s, Symbolics and LMI spun out of the AI Lab and took the code with them. They locked it up. They put lawyers on it. The shared codebase that had sustained a generation of hackers was cut in half and sold. The community fell apart.
Richard Stallman had been at the Lab since 1971. He watched it happen. He did not build another Lisp Machine. He wrote a manifesto. Software had to be free. Free to study. Free to modify. Free to share. He announced the GNU Project in 1983. He published the GNU Manifesto in 1985. The first real piece of GNU software was an editor.
GNU Emacs was not the first Emacs. Stallman and Guy Steele wrote the original EMACS at MIT in 1976. It was macros on top of TECO. James Gosling made Gosmacs for Unix in 1981. But GNU Emacs was the one that mattered. It had a real Lisp inside it. Emacs Lisp. Every buffer ran through it. Every mode. Every keystroke. The editor was a Lisp runtime that happened to show you text.
That is not a small thing. A Lisp Machine was an operating system built on a Lisp evaluator with direct access to hardware. GNU Emacs is a Lisp evaluator with direct access to an operating system. It spawns processes. It opens network sockets. It talks to the filesystem. It renders windows. It handles signals.
The Lisp Machine ran Lisp on bare metal. Emacs runs Lisp on a portable C core. That is why it survived. Four decades. Every Unix. Every window system. VAXen to Apple Silicon. The Lisp Machines sit in museums now. GNU Emacs kept going. It carried the same idea forward. The user should be able to take apart every layer of the system and put it back together differently.
(add-to-list 'load-path "~/repos/el-init")
(require 'elinit)
;; Use elinit-stop-now for immediate SIGKILL - ensures clean X session exit
(add-hook 'kill-emacs-hook #'elinit-stop-now)
;; Don't ask about running processes on exit - elinit handles cleanup
(setq confirm-kill-processes nil)
;; Start elinit and config watcher
(elinit-mode 1)Services are defined as individual unit files in your authority roots (default
~/.config/elinit.el/). For example:
;; ~/.config/elinit.el/xhost.el
(:id "xhost"
:command "xhost +SI:localuser:$USER"
:type oneshot
:required-by ("basic.target")
:tags (x-setup));; ~/.config/elinit.el/nm-applet.el
(:id "nm-applet"
:command "nm-applet"
:type simple
:wanted-by ("graphical.target")
:delay 3
:restart t
:tags (applet tray))With the above configuration running, sbin/elinitctl status shows:
$ sbin/elinitctl status ID TYPE TARGET ENABLED STATUS RESTART LOG PID REASON ---------------------------------------------------------------------------------------------------- xhost oneshot graphical.target yes done n/a yes - - nm-applet simple graphical.target yes running always yes 1396 -
M-x elinit opens an interactive dashboard for managing services. In the
following example, services and oneshots for setting up an EXWM environment.
Press ? to open the options menu.
Three transient submenus – magit-style – expose additional options for managing service lifecycles and timers, policies and inspecting state.
Each service gets its own .el file in an authority root. See
Unit Files for the full authority-root model, precedence rules, and
all available keywords.
When you first run M-x elinit, you will see a mix of target statuses that
might look surprising. Some targets show reached, others show unreachable,
and you might wonder if something is broken. It is not – this is how the
target system works. The following image shows an unconfigured elinit running
as pid2 in an Alpine Linux container.
Pressing v and V toggle these .target views in the Service sections,
hidden by default to keep the signal-to-noise ratio down.
At startup, elinit picks a root target (by default graphical.target) and
walks its dependency chain to figure out what needs to start. The built-in
target hierarchy is:
basic.target ^ (required by) multi-user.target ^ (required by) graphical.target <-- startup root
All three are in the startup path, so they activate, their member services
start, and the targets converge to reached. default.target also shows
reached because it resolves to graphical.target under the hood.
Targets like rescue.target, shutdown.target, poweroff.target, and
reboot.target are not in graphical.target’s dependency chain. Nothing
pulls them in during a normal boot, so they sit outside the activation closure.
Elinit shows these as unreachable – meaning “not part of the current
startup transaction.” This is expected. These targets only activate when you
explicitly request them with isolate or init.
Each runlevelN.target is an alias that resolves to a canonical target before
any status lookup:
| Alias | Resolves to | In normal boot? | Status |
|---|---|---|---|
runlevel2.target | multi-user.target | yes | reached |
runlevel3.target | multi-user.target | yes | reached |
runlevel4.target | multi-user.target | yes | reached |
runlevel5.target | graphical.target | yes | reached |
runlevel0.target | poweroff.target | no | unreachable |
runlevel1.target | rescue.target | no | unreachable |
runlevel6.target | reboot.target | no | unreachable |
Runlevels 2 through 5 show reached because their canonical targets
(multi-user.target and graphical.target) are in the startup path and
converged. Runlevels 0, 1, and 6 show unreachable because their canonical
targets (poweroff.target, rescue.target, reboot.target) were never pulled
into the startup transaction.
| Status | Meaning |
|---|---|
| reached | Target’s required members all started successfully. |
| degraded | Target converged, but one or more members failed. |
| converging | Target is still waiting for members to start. |
| pending | Target is in the startup path but has not begun converging yet. |
| unreachable | Target is not part of the current startup transaction. |
| masked | Target was explicitly masked by the operator. |
Each unit file is a single plist expression with :id required.
:command is required for non-target units and invalid for :type target.
| Keyword | Type | Default | Notes |
:id | string | (required) | Unit ID (non-empty string) |
:command | string or nil | (required for non-target types) | Command to execute; must be omitted for :type target |
:type | symbol | simple | simple, oneshot, or target |
:delay | non-negative number | 0 | Delays spawn in seconds |
:after | string or list of strings | nil | Ordering dependency |
:requires | string or list of strings | nil | Requirement + ordering dependency |
:enabled | boolean | t | Enable/disable service |
:disabled | boolean | nil | Inverse form of :enabled |
:restart | boolean or policy symbol | always | For simple only; accepts t/nil or always/no/on-success/on-failure |
:no-restart | boolean | nil | Inverse form of :restart (t means policy no) |
:logging | boolean | t | Per-process log capture |
:stdout-log-file | string or nil | nil | Optional stdout log path override |
:stderr-log-file | string or nil | nil | Optional stderr log path override; defaults to stdout target |
:oneshot-blocking | boolean | elinit-oneshot-default-blocking | For oneshot only |
:oneshot-async | boolean | nil | Inverse of :oneshot-blocking |
:oneshot-timeout | number or nil | elinit-oneshot-timeout | For oneshot only |
:tags | symbol, string, or list | nil | Dashboard tag filtering |
:working-directory | string | nil | Process working directory |
:environment | alist of (KEY . VALUE) | nil | Environment variables |
:environment-file | string or list of strings | nil | Environment file path(s) |
:exec-stop | string or list of strings | nil | Custom stop command(s), simple only |
:exec-reload | string or list of strings | nil | Custom reload command(s), simple only |
:restart-sec | non-negative number | nil | Per-unit restart delay, simple only |
:description | string | nil | Human-readable description, metadata only |
:documentation | string or list of strings | nil | Documentation URIs/paths, metadata only |
:before | string or list of strings | nil | Inverse ordering, see below |
:wants | string or list of strings | nil | Soft dependency, see below |
:conflicts | string or list of strings | nil | Mutual exclusion, see below |
:kill-signal | symbol or string | SIGTERM | Graceful stop signal for this unit |
:kill-mode | symbol or string | process | process or mixed, see Stop Semantics |
:remain-after-exit | boolean | nil | oneshot only: latch active on success |
:success-exit-status | int, signal symbol/string, or list | nil | simple only: extra clean exit criteria |
:user | string, integer, or nil | nil | Run-as user (requires root, trusted unit source) |
:group | string, integer, or nil | nil | Run-as group (requires root, trusted unit source) |
:wanted-by | string or list of strings | nil | Soft membership in target units |
:required-by | string or list of strings | nil | Required membership in target units |
:sandbox-profile | symbol | nil | none, strict, service, or desktop (Linux only) |
:sandbox-network | symbol | profile default | shared or isolated |
:sandbox-ro-bind | list of absolute path strings | nil | Read-only bind mounts inside sandbox |
:sandbox-rw-bind | list of absolute path strings | nil | Read-write bind mounts inside sandbox |
:sandbox-tmpfs | list of absolute path strings | nil | Tmpfs mounts inside sandbox |
:sandbox-raw-args | list of strings | nil | Raw bwrap arguments (expert gate required) |
:log-format | symbol | nil | Structured log format: text or binary |
:limit-nofile | integer, string, or infinity | nil | Max open file descriptors (RLIMIT_NOFILE) |
:limit-nproc | integer, string, or infinity | nil | Max user processes (RLIMIT_NPROC) |
:limit-core | integer, string, or infinity | nil | Max core dump size in bytes (RLIMIT_CORE) |
:limit-fsize | integer, string, or infinity | nil | Max file size in bytes (RLIMIT_FSIZE) |
:limit-as | integer, string, or infinity | nil | Max address space in bytes (RLIMIT_AS) |
Validation is performed by unit-file validation
(elinit--validate-unit-file-plist) plus
elinit--validate-entry during plan building.
- Malformed plist structures are rejected (non-proper lists, odd number of elements).
- Unknown keywords are rejected.
- Duplicate plist keys are rejected.
- Unit files require
:id.:commandis required for non-target entries and rejected for:type target. :idmust be a non-empty string containing onlyA-Z,a-z,0-9,.,_,:,@, and-.:command, when present, must be a non-empty, non-whitespace-only string.:typemust be symbolsimple,oneshot, ortarget.:stageis removed and rejected. Use:wanted-byand:required-byfor target membership.:delaymust be non-negative number.:restart, when provided, must bet/nilor one ofalways/no/on-success/on-failure.:oneshot-timeoutmust be a positive number ornil.- Boolean flag keys (
:enabled,:disabled,:logging,:no-restart,:oneshot-blocking,:oneshot-async) must be exactlytornil. :stdout-log-fileand:stderr-log-filemust be non-empty strings ornil.- Mutually exclusive pairs are rejected:
:enabledwith:disabled,:restartwith:no-restart, and:oneshot-blockingwith:oneshot-async. :restart-secwith a disabled restart policy (:no-restart t,:restart no, or:restart nil) is rejected as contradictory.- Type restrictions are enforced:
oneshotrejects:restartand:no-restart;simplerejects:oneshot-blocking,:oneshot-async, and:oneshot-timeout.oneshotrejects:exec-stop,:exec-reload, and:restart-sec(simple-only keys). :tagsmust be a symbol, string, or proper list of symbols/strings. Empty strings andnilelements within the list are rejected.:after,:requires,:before,:wants, and:conflictsmust be string or proper list of strings. Empty or whitespace-only dependency IDs are rejected. Self-referencing the entry’s own ID is rejected.:wanted-byand:required-bymust be string or proper list of strings. Referenced IDs must exist and be:type target.:working-directorymust be a string ornil.:environmentmust be an alist of(KEY . VALUE)string pairs. Keys must match[A-Za-z_][A-Za-z0-9_]*. Duplicate keys are rejected.:environment-filemust be a string, proper list of strings, ornil.:exec-stopand:exec-reloadmust be a string, proper list of strings, ornil. Empty or whitespace-only command strings within are rejected.:restart-secmust be a non-negative number ornil.:descriptionmust be a string ornil.:documentationmust be a string, proper list of strings, ornil.:kill-signalmust be a recognized signal name (e.g.,SIGTERM,SIGINT).:kill-modemust be symbolprocessormixed.:remain-after-exitmust be boolean; rejected forsimpletype.:success-exit-statusitems must be integers (0–255) or recognized signal names; rejected foroneshottype.:userand:groupmust be a string, integer, ornil. Identity requirements (root privileges, trust gate) are enforced at launch time, not during validation. See Security for details.:sandbox-profilemust be one ofnone,strict,service, ordesktop.:sandbox-networkmust besharedorisolated.:sandbox-ro-bind,:sandbox-rw-bind, and:sandbox-tmpfsmust be strings or proper lists of absolute path strings. Empty paths and forbidden destinations (/proc,/dev) are rejected. Duplicate paths are deduplicated (first occurrence wins). Bind sources (:sandbox-ro-bindand:sandbox-rw-bind) must exist on disk;:sandbox-tmpfsdestinations do not require existence (bwrap creates them).:sandbox-raw-argsmust be a list of strings. Rejected unlesselinit-sandbox-allow-raw-bwrapis non-nil. Raw args that conflict with the effective network mode or duplicate profile-managed setup are rejected (see Expert Raw Mode).- Sandbox keys are rejected for
:type target. - Units requesting sandbox (any sandbox key present or profile not
none) on non-Linux hosts are rejected. - Units requesting sandbox without
bwrap(bubblewrap) installed are rejected. :log-formatmust betextorbinary.binaryis rejected unlesselinit-log-format-binary-enableis non-nil.:log-formatis rejected for:type target.:limit-nofile,:limit-nproc,:limit-core,:limit-fsize,:limit-aseach accept a non-negative integer (sets both soft and hard), the symbolinfinity(sets both to unlimited), or a string"SOFT:HARD"where each component is a non-negative integer orinfinity. Floats, negative integers, and malformed strings are rejected. Limit keys are rejected for:type target.
Invalid entries are skipped at start and shown as invalid in dashboard
and CLI status/validate output.
ID resolution for string entries uses the basename of the first command token
(after split-string-and-unquote).
Duplicate IDs are deterministic:
- First valid occurrence wins.
- Later duplicates are skipped with warning.
:after: ordering only.:requires: pull-in + ordering.:before: inverse ordering (A :before Bis equivalent toB :after A).:wants: soft dependency with ordering preference.
Planner rules:
- Dependencies are global.
:beforeis inverted into:afteredges before sorting.- Missing
:afterand:beforerefs are dropped with warning. - Missing
:wantsrefs are dropped. - Missing
:requiresrefs are dropped for non-target units; for target units, missing:requiresmakes the target invalid. - Topological sort uses stable source order as tie-break.
- Cycle fallback clears
:after,:requires, and:wantsedges for cycle participants and falls back to deterministic source order.
:wants soft dependency semantics:
- A wanted unit that is missing, disabled, masked, or fails to start does not block the wanting unit.
:wantsdoes not force-start disabled units.
:conflicts: mutual exclusion. Starting a unit stops active conflicting units.- Conflicts are symmetric at runtime: if A declares
:conflicts "B", starting A stops B and starting B stops A. - Conflict-stopped units do not auto-restart until explicitly started.
- Missing conflict targets are dropped with warning (same as
:after). - Targets may declare
:conflicts.
Example – two audio servers that cannot coexist:
;; pipewire.el
(:id "pipewire"
:command "pipewire"
:conflicts "pulseaudio"
:wanted-by ("graphical.target"))
;; pulseaudio.el
(:id "pulseaudio"
:command "pulseaudio --start"
:conflicts "pipewire"
:wanted-by ("graphical.target")
:disabled t)Starting pipewire stops pulseaudio if it is running, and vice versa.
The stopped unit does not auto-restart until explicitly started via the
dashboard or CLI.
Valid entries are normalized to schema v1 tuple form:
(id cmd delay enabled-p restart-policy logging-p
stdout-log-file stderr-log-file
type after
oneshot-blocking oneshot-timeout tags requires
working-directory environment environment-file
exec-stop exec-reload restart-sec
description documentation before wants
kill-signal kill-mode remain-after-exit success-exit-status
user group wanted-by required-by
sandbox-profile sandbox-network sandbox-ro-bind sandbox-rw-bind
sandbox-tmpfs sandbox-raw-args
log-format
limit-nofile limit-nproc limit-core limit-fsize limit-as
conflicts)Accessor functions (elinit-entry-id, elinit-entry-command, etc.) are
the canonical way to read entry fields.
M-x elinit-verify:
- validates services
- validates timers when timer module is available
- populates invalid hashes for dashboard/CLI visibility
- opens
*elinit-verify*report buffer
M-x elinit-dry-run:
- builds plan without starting processes
- prints resolved target closure and dependency metadata
- prints timer validation summary when timers are configured
- opens
*elinit-dry-run*report buffer
Services are defined as individual unit files. Each unit file is a single .el
file containing one plist expression.
Unit files are loaded from a configurable list of authority roots, searched in order from lowest to highest precedence. When the same unit ID exists in multiple roots, the highest-precedence root wins completely (no key-level merge). Non-existent roots are silently skipped.
(setq elinit-unit-authority-path
'("/usr/lib/elinit.el/" ; Tier 1: vendor (lowest precedence)
"/etc/elinit.el/" ; Tier 2: system admin
"~/.config/elinit.el/")) ; Tier 3: user (highest precedence)The default three-tier layout mirrors systemd’s resolution order. You can add, remove, or reorder roots to suit your environment.
Unit files found across all active roots are resolved at startup using the precedence rules described below.
Each file contains a single plist:
;; ~/.config/elinit.el/nm-applet.el
(:id "nm-applet"
:command "nm-applet"
:type simple
:wanted-by ("graphical.target")
:restart t)Minimum valid keys (schema):
:id(non-empty string):command(string, for non-target units)
For operator-managed services, prefer an explicit baseline instead of relying on defaults:
:id:command:type:wanted-byor:required-by:restart
This package defaults :type to simple, :enabled to t, and :restart to
always when omitted. See Service Definition for the full key
list, defaults, and constraints.
Unit files are Lisp syntax, but they should be treated as static data declarations, not mini programs, so validation stays reliable, behavior stays reproducible, and unit loading stays deterministic.
Recommended:
- Keep values literal and explicit.
- Put conditional logic in wrapper scripts or the service program itself.
- Keep runtime behavior reproducible across machines and restarts.
Discouraged:
- Embedding logic in unit declarations (
if,when, filesystem checks, environment-dependent branching, or read-time evaluation tricks).
Good example (declarative):
(:id "backup"
:command "/usr/local/bin/backup-runner --mode=incremental"
:type oneshot
:wanted-by ("multi-user.target")
:logging t)Bad example (logic in declaration):
;; Discouraged: dynamic branching in unit data.
;; Keep this kind of logic in a script/program, not the unit file.
(:id "backup"
:command (if (file-exists-p "/mnt/backup")
"/usr/local/bin/backup-runner --mode=incremental"
"/usr/local/bin/backup-runner --mode=local")
:type oneshot
:wanted-by ("multi-user.target"))Within each authority root, unit files (*.el) are scanned in alphabetical order.
Roots are resolved in the order listed in elinit-unit-authority-path (low to
high precedence). When the same unit ID appears in multiple roots, the
highest-precedence root wins completely – no per-key merge is performed.
- Same root, duplicate IDs: first file wins, later duplicates are skipped with warning.
- Cross-root, same ID: highest-precedence root wins, lower-precedence entries are shadowed.
- ID absent from all roots: not loaded (use
edit IDto create a new unit), except default log maintenance IDs (logrotate,log-prune) whenelinit-seed-default-maintenance-unitsis enabled.
A vendor root provides a default service:
;; /usr/lib/elinit.el/polkit.el (Tier 1, vendor)
(:id "polkit"
:command "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1"
:type simple
:wanted-by ("multi-user.target"))A user root overrides it with different target membership and logging:
;; ~/.config/elinit.el/polkit.el (Tier 3, user)
(:id "polkit"
:command "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1"
:type simple
:wanted-by ("graphical.target")
:logging t)The user definition wins entirely. The vendor definition is shadowed.
If an authority root does not exist, it is silently skipped. This means a misconfigured path does not cause an error – it simply contributes no units:
(setq elinit-unit-authority-path
'("/nonexistent/root/" ; Silently skipped (does not exist)
"/etc/elinit.el/" ; Active if exists
"~/.config/elinit.el/")) ; Active if existsIf the highest-tier unit file for an ID is invalid, it blocks fallback to a valid lower-tier unit for the same ID. This prevents surprising behavior where a broken override silently reverts to the vendor default:
;; /usr/lib/elinit.el/backup.el (Tier 1, vendor -- valid)
(:id "backup"
:command "/usr/bin/backup-agent"
:type simple
:wanted-by ("multi-user.target"));; ~/.config/elinit.el/backup.el (Tier 3, user -- INVALID: missing :command)
(:id "backup"
:type simple)Result: backup appears as invalid in the dashboard and CLI. The vendor
definition is not used as a fallback – the invalid higher-tier winner blocks it.
Fix the user unit file or delete it to unblock the vendor definition.
Unit files are validated at load time:
- Unknown keywords are rejected.
- Missing
:idis rejected. :commandis required for non-target entries and rejected for target entries.:idmust be a non-empty string;:commandmust be a string when present.- File path is included in validation error messages.
- Invalid unit files are skipped and logged.
- Invalid unit files appear in the dashboard and CLI with
invalidstatus.
From the dashboard, press i e on any service row to open its unit file for
editing. If the file does not exist, a scaffold template is created with the
service ID and common configuration keys commented out.
On save, the unit file is validated automatically and results are reported in
the minibuffer. A minor mode (elinit-edit-mode) is activated with
convenient return bindings: press q to return to the *elinit*
dashboard (prompts to save if modified), or C-c C-q to save and return
unconditionally. Killing the buffer also returns to the dashboard.
From the CLI:
elinitctl edit nm-appletThis resolves the unit file path and launches $VISUAL or $EDITOR. If the
file does not exist, a scaffold is created first. If neither $VISUAL nor
$EDITOR is set, an error is returned with the unit file path for manual
editing.
From the dashboard, press c on a service row to view its unit file in
read-only mode (view-mode). Press q to return.
From the CLI:
elinitctl cat nm-appletThis outputs the raw unit file content. Returns an error if the file does not
exist. With --json, returns {"path": "...", "content": "..."}.
Create a file in your highest-precedence authority root (default
~/.config/elinit.el/). The filename should be ID.el (e.g.,
nm-applet.el):
;; ~/.config/elinit.el/nm-applet.el
(:id "nm-applet"
:command "nm-applet"
:type simple
:wanted-by ("graphical.target")
:restart t)Or press i e in the dashboard to scaffold a unit file interactively.
Run elinitctl daemon-reload to pick up the new unit file.
Startup begins from a resolved root target:
elinit-default-targetselects the startup root.- If it is
default.target, elinit resolveselinit-default-target-linkfirst. - Alias targets (for example
runlevel5.target) are resolved to canonical targets before activation.
The root must exist and be :type target. Elinit then computes an
activation closure:
- Target units pull in their own
:requiresand:wants. - Target units also pull in inverse membership edges contributed by service
:required-byand:wanted-bydeclarations. - Service units pull in their own
:requiresand:wants.
Only units in this closure are activated during startup. Units outside the
closure – including both service and target units – are shown as unreachable
in status surfaces. This applies to all unit types:
- Service and oneshot units outside the closure show
unreachable. - Target units outside the closure show
unreachable(notpending). - Alias targets (e.g.,
runlevel3.target) resolve to their canonical target before checking closure membership, so an alias showsreachedwhen its canonical target has converged, andunreachablewhen the canonical target is not in the closure.
In a normal graphical.target boot, transition targets such as rescue.target,
shutdown.target, poweroff.target, and reboot.target are not in the
activation closure and display as unreachable. This is expected – these
targets activate only when explicitly requested via isolate or init.
Scheduling is a global DAG over the activation closure.
- In-degree 0 entries are eligible to start.
- Dependents unlock when prerequisites become ready.
- Disabled entries are marked ready immediately.
- Start failures mark ready immediately (do not block graph).
- Optional
elinit-max-concurrent-startslimits active spawn attempts.
Ready semantics:
simple: ready when process spawns.oneshot: ready on exit (success/failure) or timeout.target: ready when required members converge (reached/degraded).
Startup completion requires all of:
- all entries started/skipped/failed-to-spawn,
- no pending delay timers,
- no pending blocking oneshots,
- no targets still converging.
Per-entry runtime state is tracked in elinit--entry-state.
States:
pendingwaiting-on-depsdelayeddisabledstartedfailed-to-spawnstartup-timeout
invalid is a surfaced status (from validation hash tables), not a lifecycle FSM
state in elinit--entry-state.
Invalid transitions signal errors unless forced by internal maintenance paths.
Processes are created via make-process.
- Command arguments are parsed by
split-string-and-unquote. - No implicit shell is used.
- Use explicit shell (
sh -c ...) when shell semantics are required (for example pipelinescmd1 | cmd2, redirects> file, or shell expansion like$HOME).
Restart behavior is controlled by effective restart policy (config + runtime override). The four restart policies are:
| Policy | Behavior |
|---|---|
no | Never auto-restart |
on-success | Restart only on clean exit (exit 0 or clean signal) |
on-failure | Restart only on non-clean exit |
always | Restart regardless of exit type |
Clean exit means exit code 0, or a signal in SIGHUP, SIGINT,
SIGPIPE, SIGTERM. The :success-exit-status keyword extends this
set with additional numeric exit codes and/or signal names per unit.
For example, :success-exit-status (42 SIGUSR1) treats exit code 42
and SIGUSR1 as clean exits for restart-policy evaluation.
Set the policy at runtime from the dashboard policy menu (p r, explicit
selection) or via CLI: elinitctl restart-policy always myservice.
Crash-loop protection:
- Delay:
elinit-restart-delay(overridden per-unit by:restart-sec) - Window:
elinit-restart-windowseconds - Threshold:
elinit-max-restarts - On crash-loop threshold, service is marked failed (
dead) and restart stops.
oneshot services are not auto-restarted.
Set :restart-sec to override elinit-restart-delay for a single unit.
A value of 0 means immediate retry. If nil (the default), the global
delay applies. simple only.
Set :working-directory to a path string. The process starts with that
directory as its working directory.
"~"and~/...are expanded to the home directory.- Relative paths are resolved against the directory containing the authoritative unit file for that unit.
- If the resolved directory does not exist, the process fails to start.
Effective environment build order:
- Start from inherited
process-environment. - Apply
:environment-fileentries in list order. - Apply
:environmentpairs in list order. - Later assignment for the same key overrides earlier assignment.
:environment is an alist of (KEY . VALUE) string pairs:
:environment (("APP_ENV" . "prod")
("LOG_LEVEL" . "info")):environment-file is a path (or list of paths) to files containing
KEY=VALUE lines:
- Blank lines and lines starting with
#or;are ignored. - Optional ~export ~ prefix is accepted and stripped.
- Key must match
[A-Za-z_][A-Za-z0-9_]*. - Invalid lines produce logged warnings with file:line context (non-fatal).
- Relative paths are resolved against the authoritative unit file directory.
- Leading
-in a path means a missing file is silently ignored; without it, a missing file is an error that prevents the process from starting.
When a running simple unit has :exec-reload commands, reloading it
(elinitctl reload ID, dashboard l u) runs the reload commands
sequentially without stopping or restarting the process. Each command
has a per-command timeout of elinit-shutdown-timeout seconds.
- If reload commands succeed, the unit reports
reloaded. - If any reload command fails, the unit reports an error and the process continues running.
- If
:exec-reloadisnil, reload falls back to the default behavior (stop the process, start with new config).
For oneshot completion tracking (elinit--oneshot-completed):
- normal exit stores exit code (
0,1, …) - signal death stores negative signal number (
SIGKILL->-9)
This is used by status/reporting and timer retry eligibility.
Elinit oneshot services are modeled after systemd’s Type=oneshot
services, but the mapping is not one-to-one.
| Elinit | systemd | Notes |
|---|---|---|
:type oneshot | Type=oneshot | Run-to-completion services |
:oneshot-blocking t | Type=oneshot (inherent) | Blocking is inherent to Type=oneshot; ordering deps wait for exit |
:oneshot-async t | No direct equivalent | Process runs without blocking dependency progression |
:oneshot-timeout 30 | TimeoutStartSec=30 | Kill the process if it hasn’t exited in time |
| No restart for oneshot | Restart= partially valid | systemd allows Restart=on-failure etc. for oneshot; elinit forbids all restart for oneshot |
:working-directory | WorkingDirectory= | Process working directory |
:environment | Environment= | Key-value pairs as alist |
:environment-file | EnvironmentFile= | Paths to env files; - prefix = optional |
:exec-stop | ExecStop= | Custom stop commands (simple only) |
:exec-reload | ExecReload= | Custom reload commands (simple only) |
:restart-sec | RestartSec= | Per-unit restart delay (simple only) |
:description | Description= | Human-readable description |
:documentation | Documentation= | Documentation URIs/paths |
:before | Before= | Inverse ordering dependency |
:wants | Wants= | Soft dependency |
:kill-signal | KillSignal= | Graceful stop signal |
:kill-mode | KillMode= | process or mixed (no control-group or none) |
:remain-after-exit | RemainAfterExit= | Latch active status on success (oneshot only) |
:success-exit-status | SuccessExitStatus= | Extra clean exit criteria (simple only) |
Key differences:
- Blocking is the default. Elinit oneshots block dependency progression by
default (
elinit-oneshot-default-blockingist). In systemd, blocking is inherent toType=oneshot: ordered dependencies wait for the oneshot process to exit before starting.RemainAfterExit=controls whether the unit stays in “active” state after exit, not whether it blocks ordering. - Restart policy. Elinit forbids all restart policies for
oneshot services. systemd permits
Restart=on-failure,on-abnormal,on-abort, andon-watchdogforType=oneshot(restarting on non-clean exit), but disallowsalwaysandon-success. - Timeout default. Elinit defaults to
elinit-oneshot-timeout(30 seconds). systemd disables the startup timeout for oneshot by default (TimeoutStartSec=infinity), relying on the administrator to set explicit limits. - RemainAfterExit. Elinit supports
:remain-after-exit tfor oneshot units. If the process exits with code 0, the unit status latches toactiveuntil explicitly stopped. Non-zero exit still results infailedstatus.stopon an active latched unit transitions it tostopped.starton an active unit is a no-op.restartre-runs the oneshot. - Target model and closure. Both systems sequence oneshots with dependency
ordering. Elinit activation is root-target closure based
(
:required-by~/:wanted-by~ +:requires~/:wants~), then globally topologically ordered.
Per-unit stop (elinitctl stop ID, dashboard l t):
- If the unit has
:exec-stopcommands, they run sequentially with the unit’s effective working directory and environment. Each command has a per-command timeout ofelinit-shutdown-timeoutseconds. - After stop commands complete (or fail), the process is terminated via signal.
The signal used is the unit’s
:kill-signal(defaultSIGTERM). - Stop commands failing does not abort the shutdown path.
elinit-stop (async graceful):
- Runs
:exec-stopcommand chains for applicable simple units. - Sends each unit’s
:kill-signal(defaultSIGTERM) to remaining live processes. - After
elinit-shutdown-timeout, sendsSIGKILLto survivors. For units with:kill-mode mixed,SIGKILLis also sent to discovered descendant processes of the main process.
elinit-stop-now (sync hard stop):
- sends immediate
SIGKILL - waits up to
0.5sfor process death - does not run
:exec-stopcommands - intended for
kill-emacs-hook
:kill-signal:
- Overrides the default graceful stop signal (
SIGTERM) for this unit. - Accepts signal name symbols (e.g.,
SIGQUIT,SIGUSR1). - Short forms like
QUITare normalized toSIGQUIT. - Applies to stop, restart, and shutdown paths.
:kill-mode:
process(default): signal only the main managed process.mixed: send the graceful:kill-signalto the main process first; on timeout, sendSIGKILLto both the main process and its discovered descendants.- Descendant discovery uses
list-system-processesandprocess-attributes(PID-tree traversal). If OS/process metadata is unavailable, a warning is logged and behavior falls back toprocessmode.
Four override tables are supported:
- mask override (
elinit--mask-override) - enabled override (
elinit--enabled-override) - restart override (
elinit--restart-override) - logging override (
elinit--logging)
Effective value resolution for enabled state:
- if masked, always disabled (highest precedence),
- else explicit enabled override if present,
- otherwise config default.
Enable/disable semantics follow the systemctl model:
enable ID: unit should start on nextelinit-start. Persisted as override.disable ID: unit should NOT start automatically. Persisted.start IDon a disabled unit: starts it this session only. Does not change enabled state. Only mask blocks manual start.
Overrides are persisted in elinit-overrides-file.
- Saved with atomic write (temp file + rename)
- Loaded on
elinit-start - Corrupt file is logged and preserved
Interactive helpers:
M-x elinit-overrides-loadM-x elinit-overrides-saveM-x elinit-overrides-clear
All dashboard policy commands (enable, disable, mask, unmask,
set-restart-policy, set-logging) persist overrides immediately via
elinit--save-overrides, matching the CLI behaviour.
Elinit uses systemd-style targets for startup ordering and runtime
isolation. Targets are dependency-only units (no command) whose IDs
end in .target.
Canonical built-in targets (lowest authority, user overrides win):
| Target | Requires | Description |
|---|---|---|
basic.target | (none) | Basic system initialization |
multi-user.target | basic.target | Multi-user services |
graphical.target | multi-user.target | Graphical session |
default.target | (none) | Startup root (alias resolved via elinit-default-target-link) |
rescue.target | basic.target | Single-user rescue mode |
shutdown.target | (none) | Shutdown synchronization barrier |
poweroff.target | shutdown.target | Power-off target |
reboot.target | shutdown.target | Reboot target |
Elinit maps SysV numeric runlevels to targets using systemd mapping semantics. The mapping is fixed and not configurable:
| Runlevel | Target | Description |
|---|---|---|
| 0 | poweroff.target | Halt/power off |
| 1 | rescue.target | Single-user / rescue |
| 2 | multi-user.target | Multi-user (no NFS) |
| 3 | multi-user.target | Multi-user (full) |
| 4 | multi-user.target | Reserved / multi-user |
| 5 | graphical.target | Graphical session |
| 6 | reboot.target | Reboot |
Use init N or telinit N to switch runlevels at runtime. Both
commands map the numeric runlevel to the corresponding target and
execute an isolate transaction (stops entries not in the new target
closure, starts entries in the closure).
These commands manage elinit target state only. They do not directly issue kernel poweroff or reboot syscalls.
Runlevels 0 and 6 are destructive transitions and require
confirmation. In interactive Emacs, a y-or-n-p prompt is shown.
In batch mode or JSON output, --yes is required.
Runlevels 1-5 do not require confirmation.
init N is transaction-scoped and does NOT persist default target
changes. Use set-default TARGET to persist a default target
override.
Alias targets provide alternate names for canonical targets. They resolve to the canonical target before graph expansion. The built-in runlevel aliases are:
| Alias | Resolves to |
|---|---|
runlevel0.target | poweroff.target |
runlevel1.target | rescue.target |
runlevel2.target | multi-user.target |
runlevel3.target | multi-user.target |
runlevel4.target | multi-user.target |
runlevel5.target | graphical.target |
runlevel6.target | reboot.target |
Alias resolution is immutable. User units cannot redefine
runlevelN.target alias mappings.
set-default accepts alias targets but always persists the resolved
canonical target ID. list-targets shows the KIND column
(canonical or alias) and resolved link for alias targets.
Timer targets that represent init transitions are not timer-eligible. The timer validation denylist includes:
rescue.target,shutdown.target,poweroff.target,reboot.targetrunlevel0.targetthroughrunlevel6.target
Timers targeting these IDs are rejected during validation with a
reason of the form :target 'ID' is an init-transition target and is
not timer-eligible and excluded from the scheduler.
Timer subsystem lives in elinit-timer.el and is enabled by default.
Both elinit-mode and elinit-timer-subsystem-mode must be
active for timers to run.
To disable:
(elinit-timer-subsystem-mode -1)Disabling the timer subsystem disables all timer-driven scheduling,
including logrotate-daily and log-prune-daily. It does not disable
normal unit startup.
Each timer is a plist.
Required keys:
:id(non-empty string):target(non-empty string; must resolve to aoneshot,simple, ortargetunit)
Trigger keys (at least one required):
:on-calendar:on-startup-sec:on-unit-active-sec
Optional keys:
:enabled(boolean, defaultt):persistent(boolean, defaultt)
(setq elinit-timers
'((:id "daily-backup"
:target "backup-oneshot" ;; oneshot target
:on-calendar (:hour 3 :minute 0)
:persistent t)
(:id "warm-cache"
:target "cache-prime" ;; simple service
:on-startup-sec 45)
(:id "sync-loop"
:target "sync-oneshot" ;; oneshot target
:on-unit-active-sec 300)
(:id "nightly-converge"
:target "multi-user.target" ;; target unit
:on-calendar (:hour 2 :minute 30)))):on-calendar accepts either:
- single plist, or
- list of plists (earliest next match wins)
Allowed fields:
:minute(0..59):hour(0..23):day-of-month(1..31):month(1..12):day-of-week(0..6)
Field values:
- integer
- non-empty list of integers
*wildcard
Semantics:
- next run is computed strictly after current time
- day-by-day search with bounded horizon (28-year coverage for full leap-day + weekday combinations)
- DST gaps are handled by validating encoded/decoded wall clock fields
:on-startup-sec:
- positive integer seconds after scheduler startup
- fires once per elinit session
:on-unit-active-sec:
- positive integer seconds after last successful target execution
- anchor is type-specific:
oneshot: anchored at last successful completion timestampsimple: anchored at last successful activation timestamp (spawn success oralready-activeno-op)target: anchored at last successful convergence timestamp (reachedoralready-reachedno-op)
Combined timers:
- scheduler chooses earliest due trigger among configured trigger types
When due:
- Disabled timer -> skipped, miss reason
disabled - Active target oneshot -> skipped, miss reason
overlap - Already-running simple service -> success no-op, reason
already-active - Converging target unit -> skipped, miss reason
target-converging - Disabled target unit -> skipped, miss reason
disabled-target - Masked target unit -> skipped, miss reason
masked-target - Missing target entry -> failure recorded, reason
target-not-found
Miss metadata is stored in timer state.
Configured by elinit-timer-retry-intervals (default '(30 120 600)).
- Retryable failures for oneshot targets: positive exit codes only
- Retryable failures for simple targets: spawn failures
- Retryable failures for target units: degraded convergence outcome
- Non-retryable: signal deaths (negative stored exit codes), nil, zero, overlap skips
- Retry budget resets on fresh scheduled runs
Configured by:
:persistentper timer (defaultt)elinit-timer-catch-up-limitin seconds (default 24h)
On scheduler start, persistent timers may trigger catch-up runs for missed schedules within the configured window.
Timer runtime state file: elinit-timer-state-file.
- Atomic writes (temp + rename)
- Schema versioned
- Newer incompatible schema versions are rejected
- Stale timer IDs are pruned when scheduler starts
Persisted keys include:
:last-run-at:last-success-at:last-failure-at:last-exit:last-missed-at:last-miss-reason:last-result(success,failure, orskip):last-result-reason(type-specific reason symbol or nil; e.g.already-active,already-reached,overlap,spawn-failed,target-reached,target-degraded,target-not-converged,convergence-unknown,target-converging,target-not-found,disabled,disabled-target,masked-target):last-target-type(oneshot,simple, ortarget)
Transient keys (for example :next-run-at, retry bookkeeping, startup-consumed
state) are recomputed each session.
State is saved on every trigger, completion, and scheduler stop.
- Started after startup completion
- Stopped on
elinit-stop,elinit-stop-now, and when timer mode is disabled - Uses
run-at-timescheduling (no polling loop)
Dashboard:
- Timer section is shown when
elinit-dashboard-show-timersis non-nil. - Target and tag filters apply only to the service section; the timer section
is always visible when
elinit-dashboard-show-timersis non-nil. - When the timer subsystem is disabled, the timer section renders with an explicit “(disabled)” label and no interactive timer actions.
- When no timers are configured, the section shows “no timers configured”.
CLI:
elinitctl list-timersshows timer runtime/invalid definitions.- If subsystem is gated off, command returns explicit disabled status.
Dashboard buffer: *elinit*, major mode elinit-dashboard-mode
(derived from tabulated-list-mode).
The dashboard defaults to a service-first view that hides target-type entries. This reduces noise for operators who use elinit as a service manager rather than a PID1 init system.
Press v to toggle target visibility. When targets are shown,
init-transition targets (rescue, shutdown, poweroff, reboot,
runlevel0, runlevel1, runlevel6) remain hidden by default. Press
V to include init-transition targets.
The header line always shows a bracket indicator for the current
visibility state: [services] in the default view, [services+targets]
when targets are shown, [services+targets+init] when init-transition
targets are also included.
When a target filter is active (via f), the filtered target and its
members are always shown regardless of the visibility toggle state.
IDTypeTargetEnabledStatusRestartLogPIDReason
runningactive(oneshot with:remain-after-exitexited successfully)donefaileddeadpendingstoppedmaskedinvalid
active means a :remain-after-exit oneshot exited with code 0 and is
latched active until explicitly stopped.
masked means the entry has been explicitly masked and is always disabled.
invalid means configuration/validation failure, not a runtime FSM state.
maskeddisableddelayedwaiting-on-depsfailed-to-spawnstartup-timeoutcrash-loop
Timer rows appear in a dedicated section below services with their own
column layout: ID, TARGET, ENABLED, LAST-RUN, NEXT-RUN,
EXIT, REASON, TYPE, RESULT. This matches the columns of elinitctl list-timers.
Timer rows support interactive timer actions via the y submenu.
Service-only commands (lifecycle, policy, inspect) reject timer rows
with the message: “Not available for timer rows: use timer actions
(y or ? -> Timers)”.
The first body row is a services section header:
── Services TYPE TARGET ENABLED STATUS RESTART LOG PID REASON.
Service rows follow under this section.
Service counters (run/done/pend/fail/inv) are shown in the Emacs
header line. These counters are service-only and do not include timer rows.
Timer rows do not use service STATUS~/~REASON semantics. They show
timer-specific runtime fields:
LAST-RUN: relative time since the most recent trigger (or-)NEXT-RUN: relative time until next scheduled trigger (or-)EXIT: last target exit code (or-)REASON: last result reason symbol (or-)TYPE: target type (oneshot,simple,target, or-)RESULT: last execution result (success,failure,skip, or-)
The dashboard uses a Magit-style nested menu model. Top-level keys handle
navigation, filtering, and system controls. Service actions are grouped into
three submenus (lifecycle, policy, inspect) accessed by a prefix key followed
by a second key. Timer actions use the y prefix.
| Key | Function |
f | Cycle target filter (all -> targetA -> targetB -> …) |
F | Cycle tag filter |
v | Toggle target visibility (service-first default) |
V | Toggle init-transition targets (rescue/shutdown/poweroff/reboot/runlevel0/1/6) |
g | Refresh dashboard |
G | Toggle auto-refresh (live monitoring) |
t | Open proced (system process list) |
T | Toggle proced auto-update mode |
l | Open Lifecycle submenu (service rows only) |
p | Open Policy submenu (service rows only) |
i | Open Inspect submenu (service rows only) |
y | Open Timers submenu (timer rows only) |
? | Open transient action menu |
h | Open dashboard help buffer |
q | Quit dashboard |
| Key | Function |
s | Start process |
t | Stop process (graceful, suppresses restart) |
r | Restart process (stop + start) |
k | Kill process (send signal, restart policy unchanged) |
u | Reload unit (re-read config and restart) |
f | Reset failed state |
Policy actions are explicit verbs (not blind toggles).
| Key | Function |
e | Enable entry |
d | Disable entry |
m | Mask entry (always disabled) |
u | Unmask entry |
r | Set restart policy (via selection) |
l | Set logging (via selection) |
All inspect actions are read-only.
| Key | Function |
i | Show entry details (C-u for status legend) |
d | Show dependencies for entry |
g | Show dependency graph |
b | Blame: startup timing sorted by duration |
l | View log file |
c | View unit file (read-only) |
e | Edit unit file (create scaffold if missing) |
m | Show target members (target rows only) |
Timer actions operate on timer rows only. Service rows reject these actions.
| Key | Function |
t | Trigger timer now (manual reason) |
i | Show timer details (schedule, state, retry info) |
j | Jump to target service row |
r | Reset timer runtime state and recompute next run |
g | Refresh timer section |
Timer actions are also available via the ? transient menu under
the “Timers” group.
Limitations in this version:
catandeditare not available for timer definitions (timers do not have unit files).- Timer enable/disable policy overrides are not yet supported.
- Separator rows reject service actions.
- Timer rows reject service-only commands (lifecycle, policy, inspect).
- Service rows reject timer-only commands.
?requires thetransientpackage.- Full ID is echoed in minibuffer when current row ID exceeds table width.
- Stop vs Kill:
l t(stop) gracefully terminates and suppresses auto-restart;l k(kill) sends a signal without changing restart policy. Usel sto start a stopped service again. - Restart:
l rperforms a stop-then-start cycle for service rows.
elinit-modeis the parent global mode; enabling it runselinit-start, disabling it runselinit-stop.elinit-timer-subsystem-modeis a global gate for the timer subsystem and only becomes active whenelinit-modeis also enabled.elinit-dashboard-modeis the major mode for*elinit*and can be opened independently of whether elinit is currently running.- Dashboard filters are buffer-local:
elinit--dashboard-target-filterandelinit--dashboard-tag-filter. - Dashboard auto-refresh timer (
elinit--auto-refresh-timer) is buffer-local and defaults to off until toggled withG/M-x elinit-dashboard-toggle-auto-refresh.
All dashboard faces are in customization group elinit:
| Face | Used for |
elinit-status-running | Status running |
elinit-status-done | Status done |
elinit-status-failed | Status failed |
elinit-status-dead | Status dead |
elinit-status-invalid | Status invalid |
elinit-status-pending | Status pending |
elinit-status-stopped | Status stopped |
elinit-type-simple | Type simple |
elinit-type-oneshot | Type oneshot |
elinit-type-timer | Type timer rows |
elinit-enabled-yes | Enabled column (yes) |
elinit-enabled-no | Enabled column (no) |
elinit-reason | Reason column values |
elinit-section-separator | Section header/separator rows |
The shell wrapper is transport-only; behavior is implemented in
elinit-cli.el dispatchers.
Requires the Emacs server to be running:
(require 'server)
(unless (server-running-p) (server-start))sbin/elinitctl [WRAPPER-OPTIONS] COMMAND [COMMAND-ARGS]| Option | Notes |
--help, -h | Show wrapper help |
--json | Request JSON output from CLI dispatcher |
--socket NAME, --socket-name NAME, -s NAME | Use specific local socket |
--server-file PATH, -f PATH | Use server file transport |
--timeout N, -t N | Pass wait timeout to emacsclient -w |
Wrapper transport rules:
--socketand--server-fileare mutually exclusive.--server-fileemits a TCP transport warning.
Wrapper/dispatcher usage behavior:
- Wrapper
--helpprints wrapper option help. - Calling
elinitctlwith no command prints dispatcher usage text and command list.
Systemctl-compatible commands:
status [ID...](detail with IDs, overview without)list-units [ID...](overview table)show ID(all properties of a unit)start [--target TARGET] [-- ID...]stop [-- ID...]restart [-- ID...]enable [--] ID...disable [--] ID...mask [--] ID...(always disabled, overrides enable)unmask [--] ID...kill [--signal SIG] [--] IDis-active ID(exit 0 if running or latched active, 3 if not, 4 if unknown)is-enabled ID(exit 0 if enabled, 1 if disabled/masked, 4 if unknown)is-failed ID(exit 0 if failed/dead, 1 if not failed, 4 if unknown)daemon-reload(reload unit definitions from disk)reload [--] ID...(hot-reload specific units)cat ID(display raw unit file content)edit ID(edit unit file; creates scaffold if missing)list-dependencies [ID]list-timers
Target commands:
list-targets(list all target units with convergence state and alias/canonical kind)target-status TARGET(show convergence, member lists, alias resolution)explain-target TARGET(root-cause chain for target state)isolate --yes TARGET(switch to target, transaction-scoped, does not persist)get-default(show effective default target)set-default TARGET(persist default target; aliases resolved to canonical)init [--yes] N(switch to runlevel 0-6; maps to systemd-style target)telinit [--yes] N(alias forinit)
Elinit-specific commands:
verifyreset-failed [--] [ID...](with IDs: reset those; without: reset all)restart-policy (no|on-success|on-failure|always) [--] ID...logging (on|off) [--] ID...blamelogs [--tail N] [--] IDjournal (-u ID | --unit ID | -fu ID) [-n N] [-p (err|info)] [-f|--follow] [--since TS] [--until TS] [--json]pingversion
Use -- before IDs that start with - for commands that accept positional
IDs.
status:
- with IDs: detailed per-unit output for valid units, invalid detail for misconfigured units, and “could not be found” for truly missing IDs; non-zero exit only when IDs are truly missing (not just invalid)
- without IDs: overview table (delegates to
list-units)
verify:
- returns exit code
4if service or timer validation errors exist - returns both service and timer invalid sets
reset-failed:
- with IDs: reset failed state for those entries
- without IDs: reset all failed entries
- clears crash-loop tracking so entries can be restarted
start, stop, restart:
- with IDs: operate on those IDs
- with no IDs: operate on whole elinit (
start,stop,stop+start) start --target TARGET: start full elinit from TARGET for that invocation only (cannot be combined with specific IDs)starton a disabled unit: succeeds (session-only, no override change). Only masked units are blocked from manual start.
list-dependencies:
- no ID: full edge list
- with ID: after/requires/blocks for that ID
list-timers:
- explicit disabled response when
elinit-timer-subsystem-modeis off - works even when
elinit-modeis off (shows configured timer definitions)
cat:
- requires exactly one ID argument
- outputs raw file content from the authoritative unit file (resolved via
elinit-unit-authority-path) - error if file does not exist
- JSON:
{"path": "...", "content": "..."}
edit:
- requires exactly one ID argument
- resolves path via authority roots; new files are created in the highest-precedence active root
- creates scaffold template if unit file does not exist
- reports the authority root and tier in human output and JSON
- non-interactive: launches
$VISUALor$EDITOR - JSON:
{"path": "...", "root": "...", "tier": N, "created": true/false}
daemon-reload:
- no arguments
- re-reads unit files from all authority roots and rebuilds the internal plan
- does NOT start, stop, or restart anything; runtime state is untouched
- after daemon-reload, the next
startorreloadoperates on the refreshed plan - JSON:
{"reloaded": true, "entries": N, "invalid": N} - available in dashboard transient menu under “System” group (
X)
reload:
- requires at least one ID argument
- hot-reloads specific units: re-reads config and applies changes per unit
- running simple process: stop gracefully, then start with new definition
(action:
reloaded) - not running: update stored definition only; next start uses new config
(action:
updated) - masked unit: skip with warning (action:
skipped (masked)) - unknown ID: error (action:
error: not found) - does NOT affect other units (operates only on the specified IDs)
- exit 0 if all units succeed, exit 1 if any unit has an error
- human output: one line per ID showing
ID: action - JSON:
{"results": [{"id": "x", "action": "reloaded"}, ...]} - available in dashboard transient menu under “System” group (
u)
is-active, is-enabled, is-failed:
- require exactly one ID argument
- use strict systemctl-compatible exit codes (not boolean 0/1)
- human output prints the status/state string followed by newline
is-active: exit 0 if running or latched active, exit 3 if not active, exit 4 if no such unitis-enabled: exit 0 if enabled, exit 1 if disabled or masked, exit 4 if no such unit; output distinguishes “enabled”, “disabled”, and “masked” statesis-failed: exit 0 if status is “dead” or “failed”, exit 1 if not failed, exit 4 if no such unit- JSON:
{"id": "...", "<predicate>": true/false, "status": "..."}(is-enableduses"state"instead of"status")
Human format is default.
--json returns stable object structures per command (for example status,
verify, list-dependencies, list-timers).
Status JSON top-level keys (status without IDs / list-units):
entries(array)invalid(array)
Status JSON top-level keys (status ID...):
entries(array, valid unit detail)invalid(array, invalid configured units with reason)not_found(array, truly missing IDs)
Per-entry status JSON object keys (entries array):
id,type,enabled,status,restart,logging,pid,reason,delay,after,requiresstart_time,ready_time,duration,unit_file,authority_tierworking_directory,environment,environment_file,exec_stop,exec_reload,restart_sec,user,groupsandbox_enabled,sandbox_profile,sandbox_networkuptime,restart_count,last_exit,next_restart_eta,metrics,process_treedescription,documentation,log_tail
Sandbox JSON field notes:
sandbox_enabledistruewhen sandbox is requested, otherwisefalse.sandbox_profileis always present as one of"none","strict","service", or"desktop".sandbox_networkis always present as effective mode ("shared"or"isolated"), including profile defaults when no explicit network override is set.
Timer JSON top-level keys:
timers(array)invalid(array)
Per-timer JSON object keys:
id,target,enabled,persistenton_calendar,on_startup_sec,on_unit_active_seclast_run_at,last_success_at,last_failure_atlast_exit,next_run_at,retry_atlast_miss_at,miss_reasonlast_result("success","failure","skip", ornull)last_result_reason(reason symbol string ornull)target_type("oneshot","simple","target", ornull)
Validation JSON top-level keys:
serviceswithvalid,invalid,errorstimerswithvalid,invalid,errors
Error JSON shape (for argument/runtime errors):
error(boolean)message(string)exitcode(integer)
Empty collections are encoded as arrays (not null).
| Code | Meaning |
0 | Success |
1 | Runtime failure (also: is-enabled disabled/masked, is-failed not failed) |
2 | Invalid arguments |
3 | is-active: unit exists but not active (systemctl parity) |
4 | is-*: no such unit; also: verify validation failed |
69 | Emacs server unavailable (EX_UNAVAILABLE) |
elinit-event-hook receives one plist per event:
:type(symbol):ts(float timestamp):id(string ornil):data(plist)
Event types:
startup-beginstartup-completeprocess-startedprocess-readyprocess-exitprocess-failedcleanuptimer-triggertimer-overlaptimer-successtimer-failuretarget-reachedtarget-degraded
startup-begin and startup-complete are currently defined event symbols but
are not emitted by runtime paths in this release.
By default, elinit shows warnings and errors in the minibuffer for process
failures, non-zero oneshot exits, and crash-looping services. These come from
the core logger (elinit--log), which always displays warning and error
level messages. Only info level messages are gated behind elinit-verbose.
For more granular control, use elinit-event-hook to react to structured
events:
(add-hook 'elinit-event-hook
(lambda (event)
(pcase (plist-get event :type)
('process-failed
(message "Elinit: FAILED to start %s" (plist-get event :id)))
('process-exit
(let ((code (plist-get (plist-get event :data) :code)))
(unless (eq code 0)
(message "Elinit: %s exited with code %s"
(plist-get event :id) code)))))))You can also check the dashboard or logs.
- Elinit-level log file (optional):
<elinit-log-directory>/elinit.log(controlled byelinit-log-to-file)- Per-process logs:
<elinit-log-directory>/log-<id>.logby default (controlled per service logging policy)
By default, each unit merges stdout and stderr into the same per-process
log file. Units can override stream targets with :stdout-log-file and
:stderr-log-file. If both resolve to the same path, streams remain merged.
If they differ, elinit writes stdout and stderr via separate logd writers.
These stream overrides are literal file paths; use writable paths and ensure
the target directories exist.
Example split stream configuration:
(:id "example-svc"
:command "/usr/bin/example"
:stdout-log-file "~/.config/elinit/log/example-svc.out.log"
:stderr-log-file "~/.config/elinit/log/example-svc.err.log")Log files are managed by external elinit-logd writer processes.
Each writer enforces a per-file size cap (elinit-logd-max-file-size)
and rotates locally when the limit is reached.
The default elinit-log-directory is user-local
((expand-file-name "elinit" user-emacs-directory)), so userland setups
work without requiring write access to /var/log.
If elinit-log-directory is configured to an unwritable location,
elinit automatically falls back to the same user-local default path.
If neither location is writable, file logging is skipped and services
still run.
Bundled helpers can be built from Emacs with
M-x elinit-build-libexec-helpers. Startup can prompt or auto-build
missing/stale helpers via elinit-libexec-build-on-startup.
elinit-logd and elinit-runas are deliberately narrow helper
executables. elinit-logd handles continuous append/rotate I/O outside
the Emacs Lisp scheduler loop, and elinit-runas performs privileged
identity transitions (setuid/setgid/initgroups) when units request
:user / :group.
The sbin/elinit-logrotate script provides scheduled rotation and
pruning of log files. It rotates active log files by renaming them with
a timestamp suffix (YYYYMMDD-HHMMSS), optionally signals logd writers
to reopen their files, and prunes old rotated files by age.
sbin/elinit-logrotate --log-dir /path/to/logs --keep-days 14
Options:
--log-dir DIR(required) – the log directory to operate on--keep-days N(default: 14) – prune rotated files older than N days--signal-reopen– send SIGHUP to logd writers after rotation--pid-dir DIR(default: same as--log-dir) – directory for writer PID file discovery--dry-run– print actions without executing--help– show usage
Script output behavior:
- progress/action lines are written to stdout
- warnings and errors are written to stderr
A typical cron entry for daily rotation:
0 3 * * * /path/to/elinit-logrotate --log-dir /path/to/elinit-log-directory --signal-reopen
The sbin/elinit-log-prune script enforces a hard cap on the total
size of the log directory. It deletes the oldest rotated files first
until the directory is at or below the cap. Active log files
(log-<id>.log, elinit.log) are never deleted.
sbin/elinit-log-prune --log-dir /path/to/logs --max-total-bytes 1073741824
Options:
--log-dir DIR(required) – the log directory to operate on--max-total-bytes N(default: 1073741824, i.e. 1 GiB) – hard cap on total directory size in bytes--lock-file PATH(default:<log-dir>/.prune.lock) – exclusive lock file path to prevent concurrent prune races--protect-id ID(repeatable) – never deletelog-<ID>.log; use this for services whose ID contains a timestamp pattern (e.g.svc.20250101-120000) to prevent their active log from being misidentified as rotated--dry-run– print actions without executing--help– show usage
Script output behavior:
- progress/action lines are written to stdout
- warnings and errors are written to stderr
The script uses flock for exclusive locking. If another prune instance
is already running, the script prints a lock-busy skip message and exits 0
without taking action.
Active logs whose service ID contains a timestamp pattern (e.g.
svc.20250101-120000) are unconditionally protected. A file matching
the rotated-file naming pattern is only deleted when the script can
confirm it is genuinely rotated. Confirmation requires at least one of:
- Parent-exists guard – the parent active log exists in the
directory (e.g.
log-svc.logconfirmslog-svc.20250101-120000.logis a rotated child of servicesvc). - Sibling guard – multiple rotated files share the same parent name,
confirming they are rotated children of a now-removed service (e.g.
log-svc.20240101-120000.logandlog-svc.20240201-120000.logboth map to parentlog-svc.log, confirming each other). - Children guard – the file has its own rotated children, confirming it as an active parent (protects the file from deletion).
- Open-file guard – when
fuseris available, files currently held open by a process (e.g. the logd writer) are never deleted.
A lone file with no parent, no siblings, and no children is preserved
because it could be an active log for a timestamp-like service ID.
The --protect-id flag provides additional explicit protection.
A typical cron entry for periodic pruning (e.g. hourly):
0 * * * * /path/to/elinit-log-prune --log-dir /path/to/elinit-log-directory --max-total-bytes 1073741824
Three paths keep log growth bounded end-to-end:
- logd path – each logd writer rotates its own file when the
per-file size cap is reached, then triggers the prune script
(throttled by
elinit-logd-prune-min-intervalseconds) to keep the directory within the total size cap. - Daily automatic path – a built-in
logrotateoneshot unit and a built-inlog-pruneoneshot unit are scheduled by separate timers:logrotate-dailyat 03:00 andlog-prune-dailyat 03:05. This runs automatically when the timer subsystem is enabled (the default). The rotate unit uses--signal-reopen, so log writers reopen after rotation. - Manual path –
M-x elinit-run-log-maintenanceruns the full maintenance sequence asynchronously: rotate all active logs, signal writers to reopen, then prune.
Built-in logrotate and log-prune units are inert fallback
definitions – they do not declare :wanted-by and are not activated
during startup. They exist solely so that the built-in timers
logrotate-daily and log-prune-daily can find the target entry at
runtime. When elinit-seed-default-maintenance-units is enabled
(the default), the first startup seeds unit files for these IDs into
the highest-precedence authority root.
A user-created unit file with :id "logrotate" overrides the built-in
entry, and a user-created unit file with :id "log-prune" overrides
that built-in entry. Similarly, user timers with IDs
logrotate-daily or log-prune-daily in elinit-timers override
the built-in timer schedules.
Seeded unit files are write-once: once created, they are never
overwritten by elinit. This means improvements to default
commands will not propagate to already-seeded units. To pick up
updated defaults, delete the seeded unit file and call
elinit-daemon-reload (or restart elinit) – the seeder will
create a fresh file with current defaults.
Seeding respects runtime overrides: if a maintenance unit is masked
(via elinitctl mask) or disabled (via elinitctl disable),
its unit file will not be re-seeded after deletion.
To disable automatic daily maintenance, either disable the timer subsystem entirely:
(elinit-timer-subsystem-mode -1)Or override the built-in timer with a disabled one:
(setq elinit-timers
'((:id "logrotate-daily" :target "logrotate"
:on-calendar (:hour 3 :minute 0) :enabled nil)
(:id "log-prune-daily" :target "log-prune"
:on-calendar (:hour 3 :minute 5) :enabled nil)))All paths use the same lock file (<log-dir>/.prune.lock) for prune
coordination, so concurrent invocations are safe.
Relevant customization variables:
| Variable | Default | Purpose |
|---|---|---|
elinit-logd-max-file-size | 50 MiB | Per-file size cap before logd rotates locally |
elinit-log-prune-max-total-bytes | 1 GiB | Hard cap on total log directory size |
elinit-logd-prune-min-interval | 60 s | Minimum seconds between logd-triggered prune calls |
elinit-logrotate-keep-days | 14 | Days to keep rotated files (scheduled path) |
The :log-format unit option enables structured log records.
When set, the logd writer produces machine-parseable records instead of
raw byte passthrough. Two formats are available:
text– one key=value line per record, human-readable.binary– compact binary framing with nanosecond timestamps.
Text is the default when :log-format is set. Binary requires the gate
variable elinit-log-format-binary-enable to be non-nil; units
requesting :log-format binary without the gate enabled are rejected
during validation.
Example unit file:
(:id "my-service"
:command "/usr/bin/my-service"
:log-format text)Each log line has the form:
ts=<TIMESTAMP> unit=<UNIT> pid=<PID> stream=<STREAM> event=<EVENT> status=<STATUS> code=<CODE> payload=<PAYLOAD>
Fields:
| Field | Values | Description |
|---|---|---|
ts | RFC3339Nano UTC | Record timestamp (e.g. 2026-02-16T12:34:56.123456789Z) |
unit | string | Service unit ID |
pid | integer | Process PID |
stream | stdout, stderr, meta | Output stream origin |
event | output, exit | Record type |
status | -, exited, signaled, spawn-failed | Exit status (output events use -) |
code | integer or - | Exit code (output events use -) |
payload | escaped string or - | Output data with escaping |
Payload escaping rules: \\ becomes \\\\, newline becomes \\n,
carriage return becomes \\r, tab becomes \\t, bytes outside
0x20-0x7E become \\xNN.
Binary files begin with a 4-byte magic header SLG1. Each record is:
u32be record_len (bytes after this field) u8 version (1) u8 event (1=output, 2=exit) u8 stream (1=stdout, 2=stderr, 3=meta) u8 reserved (0) u64be timestamp_ns (nanoseconds since epoch) u32be pid u16be unit_len i32be exit_code u8 exit_status (0=none, 1=exited, 2=signaled, 3=spawn-failed) u8[3] reserved (0) u32be payload_len <unit_len bytes> unit_id (UTF-8) <payload_len bytes> payload
The Elisp decoder handles truncated trailing records gracefully: it returns all valid records parsed before the truncation point and includes a warning. Unknown version or event values are rejected as hard errors.
The journal command reads and displays structured log records:
elinitctl journal -u my-service elinitctl journal -u my-service -n 20 elinitctl journal -u my-service -p err elinitctl journal -u my-service --since 1708098896 --until 1708099000 elinitctl journal -fu my-service elinitctl journal --json -u my-service
Flags:
| Flag | Description |
|---|---|
-u ID / --unit ID | Unit ID (required) |
-n N | Limit to last N records (without -n, full decoded history is returned) |
-p err / -p info | Filter by priority |
--since TS | Show records from timestamp (epoch integer or RFC3339 UTC with trailing Z) |
--until TS | Show records up to timestamp (epoch integer or RFC3339 UTC with trailing Z) |
-f / --follow | Stream new records as they arrive (requires -u/--unit) |
-fu ID | Combined follow + unit short form |
--json | JSON object output in non-follow mode; NDJSON stream in follow mode |
Priority classification: stderr output and non-clean exits (non-zero
exit code or signal death) are err; all other records are info.
Filter semantics for records missing timestamp metadata (for example
legacy fallback records): they are included even when --since or
--until is set, and -p still applies.
Auto-detection: the decoder reads the first bytes of the log file to
determine the format (SLG1 magic for binary, ts= prefix for text,
otherwise legacy raw lines). Legacy files are wrapped as unstructured
output records.
The dashboard (elinit-dashboard-view-log) also auto-detects binary
format and decodes records for display instead of showing raw binary
data.
With --json:
- non-follow mode returns one JSON object with metadata and a
recordsarray - follow mode emits NDJSON (one JSON object per line) suitable for stream consumers
The logrotate script optionally compresses rotated files with tar.
When tar is available on PATH, each newly rotated log file is
compressed into a .tar.gz archive. If compression fails, the
uncompressed file is preserved and a warning is emitted.
The prune script accepts additional aliases for consistency with journalctl vocabulary:
| Flag | Description |
|---|---|
--vacuum | Accepted as no-op alias |
--vacuum-max-total-bytes N | Alias of --max-total-bytes N |
--format-hint VALUE | Accepted for forward compatibility |
Both scripts handle .log.tar.gz files in their rotated-file patterns.
Example vacuum-style invocation:
sbin/elinit-log-prune --log-dir /path/to/logs --vacuum --vacuum-max-total-bytes 1073741824
Equivalent canonical invocation:
sbin/elinit-log-prune --log-dir /path/to/logs --max-total-bytes 1073741824
elinit--log emits levels:
errorandwarningare always shown in minibuffer/log output.infois shown whenelinit-verboseis non-nil.- When
elinit-log-to-fileis non-nil, all levels are written toelinit.logregardless ofelinit-verbose.
- Path:
elinit-overrides-file - Default:
${XDG_CONFIG_HOME}/elinit/overrides.eldifXDG_CONFIG_HOMEis set, otherwise~/.config/elinit/overrides.eld - Format: schema-versioned Elisp data
- Path:
elinit-timer-state-file - Default:
${XDG_STATE_HOME}/elinit/timer-state.eldifXDG_STATE_HOMEis set, otherwise~/.local/state/elinit/timer-state.eld - Active only when timer subsystem is active
All user options (defcustom) are listed below.
| Variable | Default | Purpose |
elinit-timers | nil | Timer definition list |
elinit-log-directory | (expand-file-name "elinit" user-emacs-directory) | Log directory |
elinit-restart-delay | 2 | Restart delay (seconds) |
elinit-max-restarts | 3 | Crash-loop threshold |
elinit-restart-window | 60 | Crash-loop time window (seconds) |
elinit-shutdown-timeout | 3 | Graceful shutdown timeout |
elinit-oneshot-default-blocking | t | Default oneshot blocking behavior |
elinit-oneshot-timeout | 30 | Default oneshot timeout |
elinit-startup-timeout | nil | Startup timeout before force-complete (nil means disabled) |
elinit-max-concurrent-starts | nil | Max concurrent startup spawn attempts |
elinit-default-target | "default.target" | Startup target to activate |
elinit-default-target-link | "graphical.target" | Alias target that default.target resolves to |
elinit-verbose | nil | Show info-level messages |
elinit-log-to-file | nil | Write elinit events to file |
elinit-watch-config | nil | Config file watch and auto-reload |
elinit-overrides-file | (XDG_CONFIG_HOME or ~/.config)/elinit/overrides.eld | Override persistence path |
elinit-logd-command | libexec/elinit-logd (relative to package) | Per-service log writer helper path |
elinit-logrotate-command | sbin/elinit-logrotate (relative to package) | Log rotation script path |
elinit-log-prune-command | sbin/elinit-log-prune (relative to package) | Global log prune script path |
elinit-logrotate-keep-days | 14 | Days to keep rotated log files |
elinit-logd-max-file-size | 52428800 (50 MiB) | Per-file size cap for log writer |
elinit-log-prune-max-total-bytes | 1073741824 (1 GiB) | Total log directory size cap |
elinit-logd-prune-min-interval | 60 | Throttle interval for logd-triggered prune (seconds) |
elinit-log-follow-interval | 1.0 | Seconds between journal follow poll cycles |
elinit-log-default-max-bytes | (* 5 1024 1024) (5 MiB) | Default tail byte cap when log decoding has no explicit record count |
elinit-logd-pid-directory | nil (falls back to effective log directory) | Writer PID file directory |
elinit-libexec-build-on-startup | prompt | Build bundled helper binaries on startup (prompt/automatic/never) |
elinit-libexec-compiler-candidates | ("cc" "clang" "gcc") | Candidate compiler commands, checked in order |
elinit-libexec-cflags | ("-Wall" "-Wextra" "-Werror" "-pedantic" "-std=c99" "-O2") | C flags used when building bundled helpers |
elinit-sandbox-allow-raw-bwrap | nil | Enable raw bwrap arguments (:sandbox-raw-args) |
elinit-log-format-binary-enable | nil | Gate variable: allow :log-format binary in unit files |
| Variable | Default | Purpose |
elinit-unit-authority-path | '("/usr/lib/elinit.el/" "/etc/elinit.el/" "~/.config/elinit.el/") | Authority roots (low to high precedence) |
elinit-unit-directory | (XDG_CONFIG_HOME or ~/.config)/elinit/units/ | Legacy unit directory (deprecated; use elinit-unit-authority-path) |
elinit-seed-default-maintenance-units | t | Auto-seed default logrotate / log-prune unit files when missing |
| Variable | Default | Purpose |
elinit-timer-state-file | (XDG_STATE_HOME or ~/.local/state)/elinit/timer-state.eld | Timer state persistence path |
elinit-timer-retry-intervals | '(30 120 600) | Retry schedule |
elinit-timer-catch-up-limit | (* 24 60 60) | Catch-up lookback window |
| Variable | Default | Purpose |
elinit-dashboard-show-header-hints | nil | Reserved compatibility option (currently no effect); use h for key help |
elinit-dashboard-show-timers | t | Show timer section |
elinit-dashboard-log-view-record-limit | 1000 | Maximum decoded records shown by elinit-dashboard-view-log |
elinit-auto-refresh-interval | 2 | Auto-refresh cadence |
| Variable | Default | Purpose |
elinit-pid1-mode-enabled | auto-detected | Non-nil means PID1 integration is active (auto-detected from pid1-mode and PID) |
elinit-pid1-mode-enabled defaults to non-nil only when Emacs was started
with --pid1 and the process is actually PID 1. In all other contexts it
defaults to nil and the PID1 module is inert.
PID1 lifecycle in elinit is single-path:
- boot hook calls
elinit-start - shutdown hooks call
elinit-stop-now
Built-in PID1 script autoloading (rc.boot.el / rc.shutdown.el) has been
removed. If you need early boot actions when initramfs does not provide them,
define an explicit early oneshot unit and place it early in the target graph
(for example :required-by ("basic.target") with :oneshot-blocking t).
You can still use pure Elisp for those actions: repurpose the example scripts
in static-builds/scripts/rc.boot.el.example and
static-builds/scripts/rc.shutdown.el.example, then invoke your script through
an explicit oneshot unit command.
When you need an early boot script:
- You usually do not need one if your initramfs already handles core early boot responsibilities before handing off to Emacs PID1 (mount, dev, fsck, networking bootstrap, runtime dirs).
- You do need one when those responsibilities are not handled before handoff and must be done deterministically before normal services.
- In that case, define an explicit early oneshot unit and wire it near the
start of the target graph (for example required by
basic.targetand blocking).
Minimal early oneshot pattern:
;; /etc/elinit.el/early-boot.el
(:id "early-boot"
:command "/usr/bin/emacs --batch -Q -l /usr/local/lib/elinit/early-boot.el"
:type oneshot
:oneshot-blocking t
:enabled t
:required-by ("basic.target"))This keeps boot sequencing explicit, testable, and visible through the same supervisor model as every other unit.
When you need shutdown or reboot scripts:
- You usually do not need one if stopping managed units is enough. In PID1
mode, signal-driven shutdown/reboot hooks already call
elinit-stop-now. - You do need one if you require deterministic pre-transition tasks under supervisor control (for example cleanup or state export).
- In that case, attach blocking oneshot units to
shutdown.target,poweroff.target, andreboot.target, then trigger transition targets explicitly withelinitctl init --yes 0(poweroff path) orelinitctl init --yes 6(reboot path).
Minimal shutdown/reboot oneshot pattern:
;; /etc/elinit.el/pre-shutdown.el
(:id "pre-shutdown"
:command "/usr/bin/emacs --batch -Q -l /usr/local/lib/elinit/pre-shutdown.el"
:type oneshot
:oneshot-blocking t
:enabled t
:required-by ("shutdown.target"))
;; /etc/elinit.el/pre-poweroff.el
(:id "pre-poweroff"
:command "/usr/bin/emacs --batch -Q -l /usr/local/lib/elinit/pre-poweroff.el"
:type oneshot
:oneshot-blocking t
:enabled t
:required-by ("poweroff.target"))
;; /etc/elinit.el/pre-reboot.el
(:id "pre-reboot"
:command "/usr/bin/emacs --batch -Q -l /usr/local/lib/elinit/pre-reboot.el"
:type oneshot
:oneshot-blocking t
:enabled t
:required-by ("reboot.target"))| Variable | Default | Purpose |
elinit-cli-follow-max-age | 3600 | Seconds before orphaned journal follow sessions are cleaned up |
| Command | Purpose |
M-x elinit-mode | Global elinit mode (start/stop + file watch) |
M-x elinit-timer-subsystem-mode | Toggle timer subsystem gate |
M-x elinit-start | Build plan and start target-closure DAG scheduler |
M-x elinit-stop | Async graceful stop |
M-x elinit-stop-now | Sync hard stop |
M-x elinit-daemon-reload | Reload unit definitions from disk into memory without runtime changes |
M-x elinit-run-log-maintenance | Run rotate, reopen, and prune immediately |
M-x elinit-build-libexec-helpers | Compile missing/stale bundled libexec helpers |
M-x elinit-verify | Verify config without start |
M-x elinit-dry-run | Show execution plan without start |
M-x elinit-migrate-config | Emit canonical schema v1 config |
M-x elinit-overrides-load | Load overrides from disk |
M-x elinit-overrides-save | Save overrides to disk |
M-x elinit-overrides-clear | Clear overrides in memory + file |
M-x elinit | Open dashboard |
M-x elinit-handbook | Open README.org handbook (read-only) |
| Command | Purpose |
M-x elinit-dashboard-lifecycle | Open lifecycle submenu |
M-x elinit-dashboard-policy | Open policy submenu |
M-x elinit-dashboard-inspect | Open inspect submenu |
M-x elinit-dashboard-refresh | Refresh dashboard buffer |
M-x elinit-dashboard-cycle-filter | Cycle target filter |
M-x elinit-dashboard-cycle-tag-filter | Cycle tag filter |
M-x elinit-dashboard-toggle-targets | Toggle visibility of target entries |
M-x elinit-dashboard-toggle-init-targets | Toggle visibility of init-transition targets (rescue/shutdown/etc) |
M-x elinit-dashboard-toggle-auto-refresh | Toggle auto-refresh |
M-x elinit-dashboard-toggle-proced-auto-update | Toggle Proced auto-update mode |
M-x elinit-dashboard-quit | Quit dashboard |
M-x elinit-dashboard-start | Start selected service |
M-x elinit-dashboard-stop | Stop selected service (graceful, suppresses restart) |
M-x elinit-dashboard-restart | Restart selected service (stop + start) |
M-x elinit-dashboard-kill | Kill selected service (send signal, restart unchanged) |
M-x elinit-dashboard-kill-force | Kill selected service (no confirm) |
M-x elinit-dashboard-reset-failed | Reset failed state |
M-x elinit-dashboard-reload-unit | Hot-reload unit at point |
M-x elinit-dashboard-daemon-reload | Reload all unit definitions from disk and refresh dashboard |
M-x elinit-dashboard-enable | Enable entry (explicit) |
M-x elinit-dashboard-disable | Disable entry (explicit) |
M-x elinit-dashboard-mask | Mask entry (always disabled) |
M-x elinit-dashboard-unmask | Unmask entry |
M-x elinit-dashboard-set-restart-policy | Set restart policy (selection) |
M-x elinit-dashboard-set-logging | Set logging (selection) |
M-x elinit-dashboard-describe-entry | Describe selected row |
M-x elinit-dashboard-show-deps | Show selected service deps |
M-x elinit-dashboard-show-graph | Show full dependency graph |
M-x elinit-dashboard-blame | Show startup timing view |
M-x elinit-dashboard-target-members | Show required/wanted members for target at point |
M-x elinit-dashboard-view-log | Open selected service log |
M-x elinit-dashboard-cat | View unit file (read-only) |
M-x elinit-dashboard-edit | Edit unit file (scaffold if missing) |
M-x elinit-dashboard-timer-actions | Open timer submenu |
M-x elinit-dashboard-timer-trigger | Trigger selected timer now |
M-x elinit-dashboard-timer-info | Show selected timer details |
M-x elinit-dashboard-timer-jump | Jump to selected timer target service |
M-x elinit-dashboard-timer-reset | Reset selected timer runtime state |
M-x elinit-dashboard-timer-refresh | Refresh timer section |
M-x elinit-dashboard-help | Open dashboard help |
M-x elinit-dashboard-menu-open | Open transient menu |
| Command | Purpose |
M-x elinit-edit-mode | Unit edit minor mode (normally enabled automatically by elinit-dashboard-edit) |
M-x elinit-edit-finish | Save unit file and return to dashboard |
M-x elinit-edit-quit | Prompt-save if modified and return to dashboard |
elinitctl uses emacsclient --eval. Which manager instance it reaches
is determined entirely by the standard emacsclient server-discovery
mechanism, which in turn depends on the caller’s OS identity:
- Each user’s Emacs server listens on a socket in that user’s runtime
directory (e.g.,
/run/user/UID/emacs). emacsclientfinds the socket belonging to the caller’s uid.- There is no elinit-level routing: manager selection = caller identity.
Operational patterns:
| Goal | Invocation |
| Manage your own services | elinitctl status |
| Manage a root-owned manager | sudo elinitctl status |
| Manage another user’s manager | sudo -u alice elinitctl status |
The --socket and --server-file wrapper options override discovery when
multiple servers coexist under one uid.
:user and :group on a unit affect the spawned process identity, not
which manager is targeted.
Explicit --system / --user scope flags (analogous to systemctl --user)
are deferred to future planning; the current model relies on caller identity
for manager selection.
elinitctl uses emacsclient --eval. Any principal that can connect to
your Emacs server can execute code as your Emacs user.
Security boundary: your Emacs server socket/server-file access controls.
Practical guidance:
- Prefer local socket transport.
- Use
--server-fileonly when you intentionally operate server-file/TCP mode. - Keep server auth/socket directories private (owner-only permissions).
When a unit specifies :user or :group, two runtime checks are enforced at
launch time (both initial startup and reload/restart):
- Root requirement. The manager must be running as root (euid 0). A
non-root manager that encounters
:useror:groupwill fail the spawn with reasonidentity change requires root (user=... group=...). - Trust gate. When running as root, the unit file must pass ownership and
permission checks before identity change is allowed:
- The unit must originate from a file on disk (not inline legacy config).
- The file must be owned by root (uid 0).
- The file must not be world-writable.
Failure produces reason
unit source not trusted (user=... group=...).
These are runtime spawn rejections, not validation errors. The unit passes
validation normally; the service appears with status stopped and a
failed-to-spawn reason containing identity context. This prevents
unprivileged users from escalating through writable unit files.
elinit provides optional process sandboxing via bubblewrap (bwrap).
Sandboxing is Linux-only, opt-in, and disabled by default.
Sandbox is configured per-unit via :sandbox-profile and optional knob
overrides. The feature model is profile-first:
- Select a curated built-in profile.
- Optionally override network mode and bind mounts.
- For advanced use, enable the global expert gate for raw arguments.
bubblewrap is an optional dependency. Units without sandbox settings launch
exactly as before. Units requesting sandbox without bwrap installed are
rejected at validation time with an explicit reason – they never fall back to
unsandboxed execution.
| Profile | Namespaces | Filesystem | Network | Use case |
|---|---|---|---|---|
none | (no sandbox) | (no sandbox) | (no sandbox) | Default – no wrapper |
strict | All (–unshare-all) | Read-only root, tmpfs /tmp | Isolated | Batch jobs, build tasks |
service | PID, IPC, UTS | Read-only root, tmpfs /tmp | Shared | Network daemons |
desktop | PID, IPC, UTS | Read-only root, tmpfs /tmp, XDG_RUNTIME_DIR, X11 socket | Shared | Desktop userland apps |
These per-unit overrides adjust the effective sandbox without bypassing the profile model:
:sandbox-network– Forcesharedorisolatednetwork mode, overriding the profile default.:sandbox-ro-bind– Add read-only bind mounts inside the sandbox.:sandbox-rw-bind– Add read-write bind mounts inside the sandbox.:sandbox-tmpfs– Add tmpfs mounts inside the sandbox.
Path validation rules:
- All paths must be absolute.
- Empty paths are rejected.
/procand/devare forbidden destinations (profiles handle these). Equivalent spellings (trailing slashes, dot segments) are canonicalized before the check.:sandbox-ro-bindand:sandbox-rw-bindsources must exist on disk.:sandbox-tmpfsdestinations do not require existence (bwrap creates them).- Duplicate paths are deduplicated (first occurrence wins).
:sandbox-raw-args allows passing arbitrary bwrap arguments. This key is
rejected at validation time unless the global gate is enabled:
(setq elinit-sandbox-allow-raw-bwrap t)When enabled, the raw arguments are appended after profile and knob arguments. Expert mode is unsupported on non-Linux and rejected.
Even with the gate enabled, conflicting or unsafe raw arguments are rejected:
--share-netis rejected when the effective network mode isisolated(explicit:sandbox-network isolatedor profile default such asstrict).--unshare-netis rejected when the effective network mode isshared(explicit:sandbox-network sharedor profile default such asservice).--unshare-all,--die-with-parent,--proc, and--devare rejected because all profiles already emit these arguments and duplication causes undefined bwrap behavior.
Sandbox status is surfaced in CLI and dashboard detail views:
- CLI human detail (
status ID/show ID) includes:Sandbox: PROFILE (network MODE)(only when sandbox is requested)
- CLI JSON entry objects include:
sandbox_enabled(boolean)sandbox_profile("none","strict","service", or"desktop")sandbox_network("shared"or"isolated")
- Dashboard inspect detail (
i i) includes:Sandbox: PROFILE (network MODE)(only when sandbox is requested)
When identity drop (:user / :group), sandbox, and resource limits are
configured, the launch wrapper ordering is fixed:
elinit-rlimits -> elinit-runas -> bwrap -> service executable
The rlimits helper applies resource limits first (outermost), then runas drops to the target identity, then bwrap creates the sandbox namespace, then the service runs inside. If only some wrappers are needed, unused layers are omitted.
;; ~/.config/elinit.el/myapp.el
(:id "myapp"
:command "/usr/bin/myapp --serve"
:sandbox-profile service
:sandbox-network shared
:sandbox-rw-bind ("/var/lib/myapp")
:description "My application (sandboxed)")- “sandbox is only supported on GNU/Linux” – bubblewrap uses Linux namespaces. Non-Linux hosts cannot use sandbox features.
- “sandbox requires bwrap (bubblewrap) but bwrap is not found in PATH” –
Install bubblewrap (e.g.,
apt install bubblewrap). - “:sandbox-raw-args requires elinit-sandbox-allow-raw-bwrap” – Set the global gate variable before loading units.
Per-service resource limits (ulimit-style) allow restricting how many file
descriptors, processes, or how much memory a managed service may use. Limits
are applied via a small C helper (elinit-rlimits) that calls
setrlimit(2) before exec-ing the service command. Linux and other POSIX
systems with setrlimit are supported.
| Key | RLIMIT constant | Typical use |
|---|---|---|
:limit-nofile | RLIMIT_NOFILE | Cap open file descriptors |
:limit-nproc | RLIMIT_NPROC | Cap child processes |
:limit-core | RLIMIT_CORE | Disable or cap core dumps |
:limit-fsize | RLIMIT_FSIZE | Cap file write size |
:limit-as | RLIMIT_AS | Cap virtual memory |
Each limit key accepts one of three forms:
- Integer – sets both soft and hard limit to the same value.
Example:
:limit-nofile 1024 - Symbol ~infinity~ – sets both soft and hard to unlimited.
Example:
:limit-core infinity - String ~”SOFT:HARD”~ – sets soft and hard independently. Each component
is a non-negative integer or the word
infinity. Example::limit-nproc "256:512"
- CLI
elinitctl show IDincludes a “Resource limits” section listing each non-nil limit. - CLI JSON output (
--json) includeslimit_nofile,limit_nproc,limit_core,limit_fsize,limit_asfields. - Dashboard inspect detail (
i i) includes a “Resource limits” section when any limit is set.
;; ~/.config/elinit.el/mydb.el
(:id "mydb"
:command "/usr/bin/mydb --datadir /var/lib/mydb"
:limit-nofile 65536
:limit-core 0
:limit-as "2147483648:4294967296"
:description "Database server with resource limits")- The
elinit-rlimitsC helper is compiled as part ofmake -C libexec check. If the helper binary is missing at launch time (e.g., compilation failed on an exotic platform), the service fails to start with an “executable not found” error rather than a validation error. - Setting
RLIMIT_NOFILEorRLIMIT_NPROCtoinfinitymay require root privileges. Other limits such asRLIMIT_COREacceptinfinityas an unprivileged user because the kernel caps it at the current hard limit. - If
setrlimitfails at runtime, the helper exits with code 112 and the service is not started.
All documentation related to this crazy idea is in ./static-builds.
Where behavior diverges across systems, parenthetical notes explain.
| Feature | systemd | runit | s6 | elinit |
|---|---|---|---|---|
| Init System (PID 1) | ||||
| Can run as PID 1 | yes (systemd is PID 1) | yes (runit-init, 3-stage boot) | yes (s6-linux-init, separate package) | yes (with --pid1 patched Emacs; also runs as PID 2+) |
| Process Supervision | ||||
| Restart crashed daemons | yes | yes | yes | yes |
| Restart policies (always/on-failure/on-success/no) | yes (6 modes) | no (always or never) | no (always or never) | yes (4 modes) |
| Restart backoff / graduated delay | yes (v254 RestartSteps) | no | no | partial (fixed restart-sec delay; no graduated steps) |
| Crash-loop detection | yes (StartLimitBurst) | no | no | yes (elinit–failed) |
| Configurable kill signal | yes (KillSignal) | yes (control/ scripts) | yes (down-signal file) | yes (kill-signal) |
| Kill mode (cgroup/process/mixed) | yes (KillMode) | no (process only) | no (process only) | yes (kill-mode, process/mixed) |
| Exec-stop custom shutdown | yes (ExecStop) | yes (finish script) | yes (finish script) | yes (exec-stop) |
| Exec-reload custom reload | yes (ExecReload) | no | no | yes (exec-reload) |
| Service Types | ||||
| Long-running daemons | yes (Type=simple) | yes | yes (longruns) | yes (type simple) |
| Oneshot run-to-completion | yes (Type=oneshot) | partial (sv once, no dedicated type) | yes (s6-rc oneshot) | yes (type oneshot) |
| Forking/double-fork PID tracking | yes (Type=forking) | no | no | no |
| D-Bus readiness type | yes (Type=dbus) | no | no | no |
| Notify readiness protocol | yes (Type=notify, sd_notify) | no | yes (notification-fd, no library needed) | no |
| Remain-after-exit | yes (RemainAfterExit) | no | no | yes (remain-after-exit) |
| Success-exit-status override | yes (SuccessExitStatus) | no | no | yes (success-exit-status) |
| Dependencies and Ordering | ||||
| Explicit dependency declarations | yes (Requires/Wants/BindsTo) | no | yes (s6-rc dependencies) | yes (requires/wants) |
| Ordering declarations | yes (Before/After) | no (manual in run scripts) | yes (s6-rc ordering) | yes (before/after) |
| Topological sort with cycle detection | yes | no | yes | yes (DAG with cycle fallback) |
| Parallel startup respecting ordering | yes | yes (all parallel, no ordering) | yes | yes (DAG in-degree scheduling) |
| Conflict declarations | yes (Conflicts) | no | no | yes (conflicts) |
| Conditional activation | yes (ConditionXxx) | no | no | no |
| Targets / Runlevels | ||||
| Named synchronization barriers | yes (targets) | partial (runlevels as dirs) | yes (s6-rc bundles) | yes (targets) |
| Runlevel/target switching at runtime | yes (isolate) | yes (runsvchdir) | yes (s6-rc -u/-d bundles) | yes (isolate, init, telinit) |
| Activation Mechanisms | ||||
| Socket activation | yes (centralized, fd passing) | no | partial (s6-ipcserver/s6-tcpserver, composable not centralized) | no |
| Timer activation | yes (OnCalendar, monotonic) | no | no | yes (on-calendar, on-startup-sec, on-unit-active-sec) |
| Path activation (inotify) | yes (.path units) | no | no (ftrig is FIFO-based, not inotify) | no |
| D-Bus activation | yes | no | no | no |
| Device activation | yes (.device from udev) | no | no | no |
| Logging | ||||
| Integrated logging daemon | yes (journald, structured binary) | yes (svlogd, plain text) | yes (s6-log, plain text) | yes (elinit-logd, text or binary) |
| Per-service log capture | yes (journal tags by unit) | yes (service/log/run pipe) | yes (servicedir/log pipe) | yes (per-id log files) |
| Log rotation | yes (journal size/time) | yes (svlogd built-in) | yes (s6-log built-in) | yes (elinit-logrotate) |
| Log pruning | yes (journald vacuum) | no (manual) | no (manual) | yes (elinit-log-prune) |
| Structured/indexed log queries | yes (journalctl field filtering) | no | no | partial (journal command with per-unit filtering; no global index) |
| Network log shipping | yes (journal-remote/upload) | partial (svlogd UDP) | no | no |
| Resource Control | ||||
| Cgroup integration | yes (native, per-unit cgroup) | no | no | no |
| CPU/memory/IO limits | yes (CPUQuota, MemoryMax, etc.) | no | no | no |
| Task limits | yes (TasksMax) | no | no | no |
| Resource accounting | yes (CPUAccounting, etc.) | no | no | no |
| Resource limits (ulimit-style) | yes (LimitNOFILE, etc.) | yes (chpst) | yes (s6-softlimit) | yes (:limit-nofile, etc.) |
| Sandboxing | ||||
| Namespace isolation | yes (PrivateTmp, PrivateNetwork, etc.) | no | no | yes (bubblewrap profiles) |
| Seccomp syscall filtering | yes (SystemCallFilter) | no | no | no |
| Capability restriction | yes (CapabilityBoundingSet) | no | no | no |
| Filesystem protection | yes (ProtectSystem, ReadOnlyPaths) | no | no | yes (sandbox-ro-bind, sandbox-rw-bind) |
| Dynamic user allocation | yes (DynamicUser) | no | no | no |
| Security audit scoring | yes (systemd-analyze security) | no | no | no |
| Sandbox profiles | partial (systemd-analyze security) | no | no | yes (none, strict, service, desktop) |
| Environment and Execution Context | ||||
| Environment variables | yes (Environment, EnvironmentFile) | partial (chpst -e envdir) | yes (s6-envdir) | yes (environment, environment-file) |
| Working directory | yes (WorkingDirectory) | yes (set in run script) | yes (set in run script) | yes (working-directory) |
| User/group execution | yes (User, Group) | yes (chpst -u) | yes (s6-setuidgid) | yes (user, group fields) |
| Supplementary groups | yes (SupplementaryGroups) | yes (chpst -u uid:gid:gid) | yes (s6-applyuidgid) | no |
| Enable / Disable / Mask | ||||
| Enable/disable services | yes (systemctl enable/disable) | yes (symlink in/out of rundir) | yes (s6-rc enable/disable) | yes (enabled/disabled, runtime overrides) |
| Mask (force-prevent start) | yes (systemctl mask) | no | no | yes (mask-override) |
| Persistent runtime overrides | partial (enable/disable persist, runtime overrides do not) | yes (symlinks persist) | yes (compiled db persists) | yes (overrides.eld atomic file) |
| Preset defaults | yes (systemd.preset files) | no | no | no |
| Drop-in override fragments | yes (unit.d/*.conf) | no | no | no |
| Template / Instance Services | ||||
| Template units with instances | yes (unit@.service, %i specifier) | no | yes (s6-instance-maker) | no |
| Watchdog | ||||
| Service heartbeat watchdog | yes (WatchdogSec, sd_notify WATCHDOG=1) | no | no | no |
| Hardware watchdog | yes (RuntimeWatchdogSec) | no | no | no |
| UI and Observability | ||||
| Interactive dashboard | no (third-party cockpit) | no | no | yes (elinit-dashboard-mode) |
| CLI status/control | yes (systemctl) | yes (sv) | yes (s6-rc, s6-svc) | yes (elinitctl) |
| JSON output mode | yes (systemctl –output=json) | no | no | yes (elinitctl –json) |
| Boot Loader | ||||
| UEFI boot manager | yes (systemd-boot) | no | no | no |
| Unified Kernel Images | yes (ukify, sd-stub) | no | no | no |
| TPM2 measured boot | yes (pcrlock, pcrextend, measure) | no | no | no |
| Networking | ||||
| Network configuration | yes (networkd) | no | no | no |
| DNS resolver/cache | yes (resolved) | no | no | no |
| NTP client | yes (timesyncd) | no | no | no |
| Container / VM Management | ||||
| OS containers | yes (nspawn) | no | no | no |
| VM spawning | yes (vmspawn) | no | no | no |
| Machine registration | yes (machined) | no | no | no |
| Portable services | yes (portabled) | no | no | no |
| System extensions | yes (sysext, confext) | no | no | no |
| Home / User / Identity | ||||
| Encrypted home dirs | yes (homed) | no | no | no |
| Declarative system users | yes (sysusers) | no | no | no |
| User record multiplexer | yes (userdb) | no | no | no |
| Hostname/locale daemons | yes (hostnamed, localed) | no | no | no |
| Storage / Filesystem | ||||
| LUKS/dm-crypt setup | yes (cryptsetup, cryptenroll) | no | no | no |
| dm-verity/dm-integrity | yes (veritysetup, integritysetup) | no | no | no |
| Declarative partitioning | yes (repart) | no | no | no |
| Disk image inspection | yes (dissect) | no | no | no |
| Credentials / Secrets | ||||
| Encrypted service credentials | yes (systemd-creds, TPM2-sealed) | no | no | no |
| OOM / Core Dumps | ||||
| Userspace OOM killer | yes (oomd, PSI-based) | no | no | no |
| Core dump capture/management | yes (coredump, coredumpctl) | no | no | no |
| Power Management | ||||
| Suspend/hibernate/hybrid | yes (systemd-sleep) | no | no | no |
| Inhibitor locks | yes (systemd-inhibit) | no | no | no |
| Soft reboot (userspace only) | yes (systemd-soft-reboot) | no | no | no |
| Misc | ||||
| Transient units from CLI | yes (systemd-run) | no | no | no |
| Boot performance analysis | yes (systemd-analyze blame/plot/critical-chain) | no | no | yes (elinitctl blame) |
| Virtualization detection | yes (systemd-detect-virt) | no | no | no |
| sudo replacement | yes (run0, v256+) | no | no | no |
| Config override delta view | yes (systemd-delta) | no | no | no |
| Unit file validation | yes (systemd-analyze verify) | no | no | yes (entry whitelist validation) |
The examples below are strict do-the-same-thing translations.
Each elinit unit includes :command in the plist, because unit files
in these examples are non-target units (where :command is required).
| systemd directive | elinit key | Notes |
|---|---|---|
Description= | :description | Same intent |
Type=simple | :type simple | Same process model |
Type=oneshot | :type oneshot | Same run-to-completion model |
ExecStart=... | :command "..." | Required in elinit unit files |
Restart=... | :restart ... | Same policy names for supported values |
RestartSec=... | :restart-sec ... | Restart delay |
WorkingDirectory=... | :working-directory ... | Same intent |
After=... | :after (...) | Use elinit IDs (for example, database not database.service) |
Requires=... | :requires (...) | Hard dependency + ordering |
Environment=K=V | :environment ((\"K\" . \"V\")) | Alist form |
ExecStop=... | :exec-stop "..." | Stop command |
ExecReload=... | :exec-reload "..." | Reload command (simple units) |
KillSignal=... | :kill-signal ... | Signal symbol |
Conflicts=... | :conflicts (...) | Mutual exclusion set |
RemainAfterExit=yes | :remain-after-exit t | Oneshot active latch |
User=~/~Group= | :user / :group | Requires root manager + trusted unit file |
LimitNOFILE= | :limit-nofile | Integer, infinity, or "SOFT:HARD" |
LimitNPROC= | :limit-nproc | Integer, infinity, or "SOFT:HARD" |
LimitCORE= | :limit-core | Integer, infinity, or "SOFT:HARD" |
LimitFSIZE= | :limit-fsize | Integer, infinity, or "SOFT:HARD" |
LimitAS= | :limit-as | Integer, infinity, or "SOFT:HARD" |
WantedBy=multi-user.target | :wanted-by ("multi-user.target") | Target membership |
systemd unit:
[Unit] Description=My Background Service After=network-ready.service [Service] Type=simple ExecStart=/usr/bin/my-daemon --config /etc/my-daemon.conf Restart=on-failure RestartSec=5 WorkingDirectory=/var/lib/my-daemon [Install] WantedBy=multi-user.target
elinit unit file (my-daemon.el):
(:id "my-daemon"
:command "/usr/bin/my-daemon --config /etc/my-daemon.conf"
:description "My Background Service"
:type simple
:wanted-by ("multi-user.target")
:after ("network-ready")
:restart on-failure
:restart-sec 5
:working-directory "/var/lib/my-daemon")systemd unit:
[Unit] Description=Create runtime directories [Service] Type=oneshot ExecStart=/usr/bin/mkdir -p /run/myapp RemainAfterExit=yes [Install] WantedBy=multi-user.target
elinit unit file (setup-dirs.el):
(:id "setup-dirs"
:command "/usr/bin/mkdir -p /run/myapp"
:description "Create runtime directories"
:type oneshot
:wanted-by ("multi-user.target")
:remain-after-exit t)systemd unit:
[Unit] Description=Web Application After=database.service Requires=database.service [Service] Type=simple ExecStart=/usr/bin/node /opt/webapp/server.js Environment=NODE_ENV=production Environment=PORT=3000 Restart=always ExecStop=/usr/bin/pkill -TERM -f /opt/webapp/server.js KillSignal=SIGTERM [Install] WantedBy=multi-user.target
elinit unit file (webapp.el):
(:id "webapp"
:command "/usr/bin/node /opt/webapp/server.js"
:description "Web Application"
:type simple
:wanted-by ("multi-user.target")
:after ("database")
:requires ("database")
:environment (("NODE_ENV" . "production")
("PORT" . "3000"))
:restart always
:exec-stop "/usr/bin/pkill -TERM -f /opt/webapp/server.js"
:kill-signal SIGTERM)systemd unit:
[Unit] Description=Web server [Service] Type=simple ExecStart=/usr/bin/nginx -g "daemon off;" User=www-data Group=www-data Restart=on-failure [Install] WantedBy=multi-user.target
elinit unit file (nginx.el), placed in a root-owned unit directory:
(:id "nginx"
:command "/usr/bin/nginx -g \"daemon off;\""
:description "Web server"
:type simple
:wanted-by ("multi-user.target")
:restart on-failure
:user "www-data"
:group "www-data")Requirements: the Emacs manager must run as root, and the unit file must be owned by root and not world-writable.
The sbin/elinit-import script automates the translation shown above.
It reads a systemd .service file and prints the equivalent elinit
plist to stdout.
sbin/elinit-import /etc/systemd/system/foo.service
Redirect to save as a unit file:
sbin/elinit-import /etc/systemd/system/foo.service > ~/.config/elinit.el/foo.el
| Flag | Description |
|---|---|
--output-dir DIR | Write output to DIR/<id>.el instead of stdout |
--dry-run | Print what would be written without writing |
--help, -h | Show usage information |
All directives from the translation table are supported: Description,
Type (simple, oneshot), ExecStart, Restart, RestartSec,
WorkingDirectory, After, Requires, Wants, Before,
Environment, EnvironmentFile, ExecStop, ExecReload,
KillSignal, KillMode, RemainAfterExit, SuccessExitStatus,
User, Group, LimitNOFILE, LimitNPROC, LimitCORE,
LimitFSIZE, LimitAS, WantedBy, and RequiredBy.
Dependency IDs are normalized: .service suffixes are stripped,
.target suffixes are preserved, and unsupported unit types
(.socket, .timer, etc.) produce a warning and are skipped.
Unsupported systemd directives (such as CapabilityBoundingSet or ProtectSystem)
produce a warning on stderr and are skipped. Unsupported Type values
(forking, notify, dbus, idle) produce a warning and default to simple.
Systemd exec prefixes (-, +, !, !!) on ExecStart are
stripped automatically. Quoted Environment values
(Environment="KEY=VALUE") are handled correctly.
Given /etc/systemd/system/webapp.service:
[Unit] Description=Web Application After=database.service Requires=database.service [Service] Type=simple ExecStart=/usr/bin/node /opt/webapp/server.js Environment=NODE_ENV=production Environment=PORT=3000 Restart=always RestartSec=5s ExecStop=/usr/bin/pkill -TERM -f /opt/webapp/server.js KillSignal=SIGTERM LimitNOFILE=65536 [Install] WantedBy=multi-user.target
Running:
$ sbin/elinit-import /etc/systemd/system/webapp.service
Produces on stdout:
(:id "webapp"
:command "/usr/bin/node /opt/webapp/server.js"
:description "Web Application"
:type simple
:wanted-by ("multi-user.target")
:after ("database")
:requires ("database")
:restart always
:restart-sec 5
:environment (("NODE_ENV" . "production")
("PORT" . "3000"))
:exec-stop "/usr/bin/pkill -TERM -f /opt/webapp/server.js"
:kill-signal SIGTERM
:limit-nofile 65536)No warnings are produced because all directives have elinit equivalents. When unsupported directives are present, the skipped-directive summary on stderr lists every line that had no elinit equivalent.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Runtime error (file not found, missing ExecStart) |
| 2 | Invalid arguments |
The manager is not running as root. :user and :group require the Emacs
process to have euid 0 so that the elinit-runas helper can call
setuid/setgid.
Resolution: start Emacs (and its server) as root, or remove the :user /
:group keys from the unit file.
The manager is root but the unit file failed the trust gate. Common causes:
- The unit file is not owned by root. Fix:
sudo chown root:root unit.el. - The unit file is world-writable. Fix:
sudo chmod o-w unit.el. - The unit was defined inline in
elinit-programsrather than in a unit file. Inline definitions have no verifiable file ownership; move the definition to a root-owned unit file.
When :user or :group is set, the command is launched through the
elinit-runas helper (at libexec/elinit-runas). This error means
either the helper itself or the target executable could not be found. Verify
that elinit-runas-command points to the compiled helper and that the
target program path is absolute (the helper uses execv without PATH search).
Every state-mutating entry point across all modules, classified by semantic category. Classification key:
- Explicit verb: user-invoked command that performs exactly one named operation on a named target.
- Unit-definition mutation: re-reads or modifies the on-disk unit definition; state changes are a side-effect of the new definition.
- Implicit (sentinel): triggered automatically by a process exit event; not directly user-invoked.
- Implicit (scheduler): triggered automatically by the staging engine during startup sequencing; not directly user-invoked.
- UI-local state: mutates dashboard-local or display state only (buffer-local variables, timers); does not change service runtime state.
No toggle/cycle mutators for service policy remain. Legacy toggle
commands (toggle-restart, toggle-enabled, toggle-mask,
toggle-logging) were removed in favour of explicit verb replacements.
Public entry points that change runtime state:
| Function | Category | State mutated |
|---|---|---|
elinit-start | Explicit verb | Clears all runtime state, loads overrides, builds plan, starts target-closure DAG scheduler |
elinit-stop | Explicit verb | Sends graceful stop to all processes (async) |
elinit-stop-now | Explicit verb | Sends hard stop to all processes (sync, for kill-emacs-hook) |
elinit-daemon-reload | Unit-definition mutation | Re-reads unit files, refreshes invalid hash |
elinit-overrides-load | Explicit verb | Reads override hash tables from elinit-overrides-file |
elinit-overrides-save | Explicit verb | Writes override hash tables to elinit-overrides-file |
elinit-overrides-clear | Explicit verb | Clears all override hash tables and deletes file |
Private helpers called by CLI and dashboard (not direct entry points):
| Function | Category | State mutated |
|---|---|---|
elinit--manual-start | Explicit verb | Clears failed/stopped flags, spawns process, records manually-started |
elinit--manual-stop | Explicit verb | Stops process, records manually-stopped, clears remain-active latch |
elinit--manual-kill | Explicit verb | Sends signal to process, restart policy unchanged |
elinit--reset-failed | Explicit verb | Clears failed, oneshot-completed, restart-times for target |
elinit--reload-unit | Unit-definition mutation | Re-reads unit file, stops/starts if running, clears stale state |
elinit--policy-enable | Explicit verb | Sets/clears enabled-override with config normalization |
elinit--policy-disable | Explicit verb | Sets/clears enabled-override with config normalization |
elinit--policy-mask | Explicit verb | Sets mask-override |
elinit--policy-unmask | Explicit verb | Clears mask-override |
elinit--policy-set-restart | Explicit verb | Sets/clears restart-override with config normalization, cancels timer on no |
elinit--policy-set-logging | Explicit verb | Sets/clears logging with config normalization |
elinit--save-overrides | Implicit (scheduler) | Writes override hash tables to elinit-overrides-file (called by policy commands) |
elinit--check-crash-loop | Implicit (sentinel) | Records restart timestamp; marks failed if threshold exceeded |
elinit--make-process-sentinel | Implicit (sentinel) | On exit: removes from processes, records last-exit-info, handles oneshot completion, schedules restart |
elinit--start-process | Implicit (scheduler) | Inserts into processes, clears manually-stopped, records start-times |
elinit--handle-oneshot-exit | Implicit (sentinel) | Records oneshot-completed exit code, sets remain-active latch |
elinit--transition-state | Implicit (scheduler) | Updates entry-state FSM |
elinit--reconcile | Implicit (daemon-reload) | Diffs running vs desired state, stops/starts as needed |
All CLI subcommand handlers. Each dispatches to a core helper.
| Subcommand | Function | Category | Core call | Persists |
|---|---|---|---|---|
start | elinit--cli-cmd-start | Explicit verb | elinit--manual-start | No |
stop | elinit--cli-cmd-stop | Explicit verb | elinit--manual-stop / elinit-stop | No |
restart | elinit--cli-cmd-restart | Explicit verb | elinit--manual-stop + elinit--manual-start | No |
kill | elinit--cli-cmd-kill | Explicit verb | elinit--manual-kill | No |
enable | elinit--cli-cmd-enable | Explicit verb | elinit--policy-enable + save-overrides | Yes |
disable | elinit--cli-cmd-disable | Explicit verb | elinit--policy-disable + save-overrides | Yes |
mask | elinit--cli-cmd-mask | Explicit verb | elinit--policy-mask + save-overrides | Yes |
unmask | elinit--cli-cmd-unmask | Explicit verb | elinit--policy-unmask + save-overrides | Yes |
restart-policy | elinit--cli-cmd-restart-policy | Explicit verb | elinit--policy-set-restart + save-overrides | Yes |
logging | elinit--cli-cmd-logging | Explicit verb | elinit--policy-set-logging + save-overrides | Yes |
reset-failed | elinit--cli-cmd-reset-failed | Explicit verb | elinit--reset-failed | No |
reload | elinit--cli-cmd-reload | Unit-definition mutation | elinit--reload-unit | No |
daemon-reload | elinit--cli-cmd-daemon-reload | Unit-definition mutation | elinit-daemon-reload | No |
All interactive dashboard commands that change state.
| Command | Category | Core call | Persists |
|---|---|---|---|
elinit-dashboard-start | Explicit verb | elinit--manual-start | No |
elinit-dashboard-stop | Explicit verb | elinit--manual-stop | No |
elinit-dashboard-restart | Explicit verb | elinit--manual-stop + elinit--manual-start | No |
elinit-dashboard-kill | Explicit verb | elinit--manual-kill | No |
elinit-dashboard-kill-force | Explicit verb | elinit--manual-kill (no confirm) | No |
elinit-dashboard-reset-failed | Explicit verb | elinit--reset-failed | No |
elinit-dashboard-enable | Explicit verb | elinit--policy-enable + save-overrides | Yes |
elinit-dashboard-disable | Explicit verb | elinit--policy-disable + save-overrides | Yes |
elinit-dashboard-mask | Explicit verb | elinit--policy-mask + save-overrides | Yes |
elinit-dashboard-unmask | Explicit verb | elinit--policy-unmask + save-overrides | Yes |
elinit-dashboard-set-restart-policy | Explicit verb | elinit--policy-set-restart + save-overrides | Yes |
elinit-dashboard-set-logging | Explicit verb | elinit--policy-set-logging + save-overrides | Yes |
elinit-dashboard-reload-unit | Unit-definition mutation | elinit--reload-unit | No |
elinit-dashboard-daemon-reload | Unit-definition mutation | elinit-daemon-reload | No |
elinit-dashboard-cycle-filter | UI-local state | Sets buffer-local elinit--dashboard-target-filter | No |
elinit-dashboard-cycle-tag-filter | UI-local state | Sets buffer-local elinit--dashboard-tag-filter | No |
elinit-dashboard-toggle-auto-refresh | UI-local state | Creates/cancels buffer-local auto-refresh timer | No |
elinit-dashboard-toggle-proced-auto-update | UI-local state | Toggles proced-auto-update-flag | No |
Every CLI mutator has a dashboard counterpart with identical semantics.
| Operation | CLI | Dashboard | Same core path |
|---|---|---|---|
| Start | start ID | l s | elinit--manual-start |
| Stop | stop ID | l t | elinit--manual-stop |
| Restart | restart ID | l r | stop + start |
| Kill | kill ID | l k | elinit--manual-kill |
| Reset failed | reset-failed ID | l f | elinit--reset-failed |
| Enable | enable ID | p e | elinit--policy-enable |
| Disable | disable ID | p d | elinit--policy-disable |
| Mask | mask ID | p m | elinit--policy-mask |
| Unmask | unmask ID | p u | elinit--policy-unmask |
| Restart policy | restart-policy ID POLICY | p r | elinit--policy-set-restart |
| Logging | logging ID on\vert{}off | p l | elinit--policy-set-logging |
| Reload unit | reload ID | l u | elinit--reload-unit |
| Daemon reload | daemon-reload | X (transient) | elinit-daemon-reload |
All policy mutations (enable/disable/mask/unmask/restart-policy/logging)
route through shared core policy mutators (elinit--policy-*) that
validate entry existence, reject invalid entries, normalize overrides
against config defaults, and return a status plist. Both CLI and
dashboard call elinit--save-overrides after the core mutator
applies the change.
make check
make lint
make test
make test-one TEST=elinit-test-parse-string-entry
make pid1-check ELINIT_PID1_EMACS=/path/to/patched/emacspid1-check is optional and skips when prerequisites are unavailable
(patched Emacs binary with --pid1, unshare, user namespaces).
Interactive load:
emacs -Q -l elinit.elThis is a topical index for targeted lookup of less-obvious behavior.
- Activation closure and stage progression: Startup and Lifecycle Model, Target System
- Alias targets and runlevel mapping: Target System
- Async oneshots (
:oneshot-async,:oneshot-blocking): Service Definition, Startup and Lifecycle Model - Blocking oneshot timeout (
:oneshot-timeout,elinit-oneshot-timeout): Service Definition, Startup and Lifecycle Model, Customization Reference - Bubblewrap sandbox profiles and safe knobs: Process Sandbox (bubblewrap), Service Definition
- Bubblewrap raw mode and conflict guards: Expert Raw Mode, Service Definition
- Built-in targets and default target chain: Target System
- Cat unit file (
cat,i c): Unit Files, Dashboard (M-x elinit) - CLI
journalcommand and follow mode (-f): CLI (sbin/elinitctl), Persistence and Files - CLI JSON contracts and error envelope: CLI (
sbin/elinitctl) - Command parsing (no implicit shell; use
sh -cfor shell operators): Startup and Lifecycle Model - Config file watch and debounce (
elinit-watch-config): Persistence and Files, Customization Reference - Crash-loop detection (
elinit-max-restarts,elinit-restart-window): Startup and Lifecycle Model, Customization Reference - Cycle fallback behavior for dependency graphs: Service Definition, Startup and Lifecycle Model
- Dashboard filtering (target/tag): Dashboard (
M-x elinit) - Dashboard live updates (
elinit-auto-refresh-interval): Dashboard (M-x elinit), Customization Reference - Dashboard mode/filter state (buffer-local target/tag/auto-refresh): Dashboard (
M-x elinit) - Dashboard service-first default view: Dashboard (
M-x elinit) - Dashboard timer rows and timer actions: Dashboard (
M-x elinit), Timer Subsystem - Dashboard visual customization (faces): Dashboard (
M-x elinit), Customization Reference - Daemon reload and reconcile semantics: Runtime Overrides and Reconciliation, CLI (
sbin/elinitctl) - Dependency semantics split (
:after/:requires/:before/:wants): Service Definition - Disabled unit manual start (session-only, systemctl model): Runtime Overrides and Reconciliation
- Edit unit file (
edit,i e, scaffold template): Unit Files, Dashboard (M-x elinit) - Environment variables and files (
:environment,:environment-file): Startup and Lifecycle Model, Service Definition - Event API (
elinit-event-hookand event types): Events and Hooks - Exit code semantics for oneshots (signals stored negative): Startup and Lifecycle Model
- Graceful stop vs immediate kill (
elinit-stopvselinit-stop-now): Startup and Lifecycle Model, Command Reference - Hyphen-prefixed IDs in CLI (use
--separator): CLI (sbin/elinitctl) - ID resolution and duplicate handling: Service Definition, Unit Files
- Integrated log maintenance units (
logrotate,log-prune): Persistence and Files - Libexec helper rebuild policy (
elinit-libexec-build-on-startup): Customization Reference - Log file paths, per-stream logs, and log directory behavior: Persistence and Files, Service Definition
- Logging pipeline (
elinit--log,elinit-verbose,elinit-log-to-file): Persistence and Files, Customization Reference - Logging structured formats (
:log-format text|binary): Service Definition, Persistence and Files, Dashboard (M-x elinit) - Manager targeting (caller identity,
emacsclientdiscovery): Security - Mask/unmask semantics: Runtime Overrides and Reconciliation
- Max concurrent startup spawn attempts (
elinit-max-concurrent-starts): Startup and Lifecycle Model, Customization Reference - Mutator inventory and classification (explicit verb vs unit-definition): Service Management Systems Comparison
- Override precedence (runtime overrides vs unit defaults): Runtime Overrides and Reconciliation
- Override persistence (
elinit-overrides-file): Runtime Overrides and Reconciliation, Persistence and Files - PID1 mode, static builds, Emacs patches, deterministic early boot units: Customization Reference
- Privilege-drop trust gate for
:user/:group: Security, Service Definition - Privilege-drop troubleshooting (identity errors, trust gate failures): Privilege-Drop Troubleshooting
- Reload unit (
reload, dashboardu): CLI (sbin/elinitctl), Dashboard (M-x elinit) - Resource limits (
:limit-*keys, syntax, status surfaces): Resource Limits, Service Definition - Security boundary (
emacsclient --eval, socket/server-file access): Security - Service-management comparison matrix: Service Management Systems Comparison
- Startup timeout force-complete (
elinit-startup-timeout): Startup and Lifecycle Model, Customization Reference - systemd conversion examples and mapping notes: Example Conversions: systemd to elinit
- systemd import converter (
sbin/elinit-import): Automated Import (sbin/elinit-import) - Target reachability semantics in dashboard: Understanding the Dashboard View, Target System
- Timer catch-up window (
elinit-timer-catch-up-limit,:persistent): Timer Subsystem, Customization Reference - Timer denylist for init-transition targets: Target System
- Timer disabled/gated behavior (
elinit-timer-subsystem-mode+ parent mode): Timer Subsystem, Command Reference - Timer miss reasons (
overlap,disabled,disabled-target,masked-target,target-not-found): Timer Subsystem, Dashboard (M-x elinit) - Timer retry policy (
elinit-timer-retry-intervals, signal non-retry): Timer Subsystem, Customization Reference - Timer state persistence and schema compatibility (
elinit-timer-state-file): Timer Subsystem, Persistence and Files - Unit file format, required keys, and validation flow: Unit Files, Service Definition
- Unit file validate-on-save in edit buffers: Unit Files
- Wrapper help output vs no-command usage output: CLI (
sbin/elinitctl) - Wrapper launch order (
elinit-rlimits -> elinit-runas -> bwrap -> executable): Process Sandbox (bubblewrap), Resource Limits
GPL-3.0-or-later. See LICENSE.