Hooks that run without error and still report a missing file are usually looking in the wrong place, not failing at their job. Relative paths in a script resolve against the process's working directory, and a service starts with a working directory you did not choose. The file next to the script and the file the process opens are only the same thing when something anchors them. That gap accounts for a large share of hook failures that get misread as permission problems.

Check the path before the permissions, because a permission error on the wrong file is still the wrong file.

A relative path resolves against the process, not the file

When a script calls open("policy.json"), the kernel resolves the name against the current working directory of the process. Nothing about the script's own location participates in that lookup. OpenVPN starts hooks from a directory it chooses, and the cd option can change it, so a script tested by hand from its own folder behaves differently once the daemon runs it. The manual test passes, the service run fails, and the log line mentions only the missing name.

Python exposes the script's location as __file__, which is what makes an anchor possible. Path(__file__).resolve().parent / "policy.json" builds a path from the file rather than from the process. That one line removes the working-directory dependency without changing anything about privileges.

Anchor the path deliberately, then check what resolve does

resolve() follows symbolic links, so an installation that links the hook into place anchors at the link target, not at the directory holding the link. That is usually what you want for a versioned deployment, and it is wrong for a layout that expects data beside the symlink. Decide which one the deployment means, and write the code to match rather than discovering the difference in production. Path(__file__).parent without resolve() keeps the link's own directory.

If the data belongs to the instance rather than to the program, take an absolute path from configuration. Hard-coding a location couples the hook to one host layout.

Walk four boundaries in order

Start with the working directory: compare the unit's WorkingDirectory, OpenVPN's cd option, and any wrapper that changes directory before exec. Then confirm the resolved location, because a similarly named file in another deployment can answer the lookup and hide the mistake. Next comes the filesystem view: a chroot or a service mount namespace can make an absolute host path simply absent. Last is the access identity, which is a separate question from visibility.

ProtectHome and PrivateTmp hide different locations, and neither shows up in a directory listing taken from the host. A path that exists for root in a shell can be invisible inside the unit's namespace, which is why testing the same command outside the service proves so little.

Fix the lookup without widening the service

Anchoring the path solves only the first boundary. It does not expose a directory the service hides, and it does not grant an unprivileged account access to a root-only file. Raising script-security changes which scripts OpenVPN will run; it has no effect on where a relative name resolves. Changing the working directory for the whole service is broader than the problem, because every other relative certificate, log or helper path moves with it.

Prefer the narrow fix: a deterministic path in the script, an explicit configuration value for instance data, and file ownership that matches the account the hook actually runs as.

Record evidence that names the boundary

A useful failure report contains three things: the exact path the hook attempted, the environment that produced it, and which boundary refused. Print the resolved path and the exception without dumping the file's contents. That triple separates a wrong anchor from a hidden mount from a permission denial, which are three different fixes.

The claim that root can read the file from a terminal answers one of those questions and none of the others. Test from the service's own context instead.

Ngaahi tokoni fekau'aki