A service unit can look harmless in a change review: a new ExecStart path, a dependency on a target, or one hardening directive. The failure often arrives after someone has copied the file into the system unit directory and run systemctl daemon-reload. At that point the change is mixed with every other unit file the manager discovers, and an operator has to decide whether the diagnostic belongs to the proposed service or to an older file already on the host.
systemd-analyze verify provides a smaller boundary before that reload. It parses unit files and checks items such as executable paths without starting the service. The useful result is not merely that a file is valid-looking text. A good candidate should exit zero, while a deliberately broken executable path should exit nonzero and name the unit that needs attention.
This procedure was tested locally on Linux AMD64 with systemd 257.13. The test used temporary synthetic service files, did not run daemon-reload, and did not start or modify a service. One valid unit exited zero. Changing only ExecStart to a nonexistent executable made the same check exit one. The test also found that --recursive-errors=no keeps the check focused on the requested candidate when unrelated installed units have their own diagnostics. Use the systemd version deployed on the host that will load the file; parser behavior and available directives can differ between distribution releases.
Systemd v259.8 was published on 24 July 2026. That release is a useful reason to record the parser version used in a maintenance gate, not a reason to upgrade a working service manager without testing its full host compatibility. Check the release notes and the manual for the version your distribution actually supplies.
Start with a unit that has a narrow job
Put the proposed unit in a temporary review directory first. The example is a one-shot service whose command is deliberately harmless. The network ordering is included to show the difference between ordering and a requirement: Wants= requests the target without making its failure fatal to this service, while After= keeps this service from being ordered ahead of it.
# <review-directory>/example-gate.service
[Unit]
Description=Example parser-validation gate
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/true
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
[Install]
WantedBy=multi-user.target
Type=oneshot is appropriate for a command that completes and exits. It is not a substitute for the long-running service type an actual daemon needs. NoNewPrivileges=yes prevents the process from gaining privileges through mechanisms such as set-user-ID executables. PrivateTmp=yes gives the service a private temporary-files view, and ProtectSystem=strict makes much of the operating-system hierarchy read-only. These are examples, not a universal baseline. A service that writes state, manages devices, or needs to change system configuration may need different permissions.
Keep the unit outside /etc/systemd/system until it passes the check. That prevents a review-time typo from becoming part of the next manager reload. It also makes the test safe to run in CI or on an administrator workstation, provided the referenced executable path is meaningful for the target system.
Run the parser check without widening its scope
First record the tool that will evaluate the file:
systemd-analyze --version
Then verify the candidate. Substitute the actual temporary path for <review-directory>.
systemd-analyze verify --recursive-errors=no \
<review-directory>/example-gate.service
On the tested systemd 257.13 installation, the valid file produced no diagnostic text and exited with status zero. In shell automation, make the status visible rather than assuming a quiet command passed:
if systemd-analyze verify --recursive-errors=no \
<review-directory>/example-gate.service; then
printf '%s\n' 'unit-file verification passed'
else
printf '%s\n' 'unit-file verification failed' >&2
exit 1
fi
The verify verb is a preflight, not an activation command. It does not demonstrate that DNS works, that a remote dependency is reachable, that a user or group exists, or that the service will complete its real workload. It does catch problems that systemd can determine from the unit-file parse and local executable checks before an operator changes the manager’s loaded configuration.
The --recursive-errors=no option matters on a host with a backlog of unrelated unit warnings. A validation run without that scope control reported diagnostics from another installed service in addition to the requested file. Repeating the requested-file check with --recursive-errors=no made the valid candidate quiet and retained the meaningful failure for the invalid candidate below. That makes CI output more attributable: a proposed unit should fail the proposed-unit gate for its own issue, while existing host debt can be tracked separately.
Do not use the narrower output to hide errors from a full-host unit-file audit. Run a broader scheduled check when that is the goal. The focused invocation is for reviewing one change before it is installed or reloaded.
Prove that the gate fails for the mistake you care about
A passing file alone does not prove the command is protecting the boundary. Copy the candidate and change only its executable path:
# <review-directory>/broken-gate.service
[Unit]
Description=Example parser-validation failure
[Service]
Type=oneshot
ExecStart=/usr/bin/this-command-does-not-exist
[Install]
WantedBy=multi-user.target
Run the same check against the disposable failure fixture:
systemd-analyze verify --recursive-errors=no \
<review-directory>/broken-gate.service
printf 'exit status: %s\n' "$?"
The local systemd 257.13 test returned status 1 and reported that bad-gate.service used a command that was not executable because the path did not exist. The exact service name and wording will vary with the filename and systemd version, but the nonzero result is the contract a review gate should preserve.
Do not add || true around this command in a pipeline. Correct the unit’s path, permissions, package dependency, or generator output, then run the same check again. If the real service uses a binary installed by another package or release artifact, test after that artifact is present in an equivalent staging image. A parser check on a workstation cannot prove that a package path will exist on a different operating-system release.
Add a separate hardening review when it fits the service
systemd-analyze security evaluates service sandboxing choices. It answers a different question from verify: a unit can parse correctly while still running with a broad privilege and filesystem exposure. For an offline review of a candidate file, use:
systemd-analyze security --offline=yes \
<review-directory>/example-gate.service
The tested synthetic unit exited zero and reported an overall exposure score of 8.9, despite using NoNewPrivileges, PrivateTmp, and ProtectSystem. That is expected for a deliberately small example with no dedicated user, capability restrictions, address-family controls, or syscall filter. Treat the score as a review aid rather than a release criterion copied between services. Tighten a real unit only after identifying its required files, sockets, device access, credentials, and lifecycle behavior, then test its actual workload in an authorized environment.
Once the candidate passes its focused verification, install it through the deployment mechanism that owns the host. Only then run the documented daemon-reload and service-specific startup or restart action. Follow that state-changing step with systemctl status, journal inspection, and an application-level health check. The preflight has done its job if it keeps trivial unit-file mistakes out of that more consequential stage.