Execution Locks
Use [locks] to serialize operations that share build output, a test database,
a deployment endpoint, or another exclusive resource. Permissions decide whether
a call may run; locks decide when an authorized call may run.
[permissions]
approve = ["exec(cargo) args(build)"]
[locks]
build-output = [
"exec(cargo) args(*)",
"exec(make) args(*)",
"shell(make check)",
"background(make check)",
]
cargo build is approved by this example. Another Cargo invocation still needs
approval, but every authorized Cargo invocation takes build-output. Session
approvals, remembered approvals, and --yes do not bypass resources. Denied and
malformed calls acquire nothing. When a call needs approval, its permission
dialog or plain-text prompt lists every selected name as Locks if approved:
before the call. The preview acquires nothing and is omitted when no names
match; approved execution uses the same selected set.
Names and matching
Names match [a-z][a-z0-9_-]{0,63}. An absent or empty table acquires nothing;
an empty pattern array selects no calls. Invalid names and patterns fail config
loading rather than being ignored.
Patterns use the permission syntax. Direct exec calls pin
the program and can glob the space-joined arguments. Shell and background
patterns are exact command strings, without wildcards. Bare tool names select
every call of that tool. Every matching resource applies; there is no
first-match precedence.
exec(cargo) does not match shell(cargo build). shell(make check) does not
match background(make check). Match the outer operation, not subprocesses or
commands hidden inside scripts. Ordinary tools can have lock patterns too.
Gates, including final publication gates, match by their command form. Arrays
such as command = ["cargo", "test"] match exec(cargo) args(test) and
exec(cargo) args(*). Strings match shell(<expanded command>). For a gate
with {files}, matching uses the final argument vector or shell-quoted
expansion. Bare shell conservatively covers all shell operations, including
string gates and user commands, but not array gates. There
is no separate gate-lock field.
Within one gate run, a successful gate keeps resources also needed by the next executed gate. Skipped gates do not break retention. Other resources release before that next gate; failure, cancellation, and the end of the run release all resources after cleanup. If an additional resource is busy, heph releases the retained set and retries the complete set to avoid deadlocks. This applies to acceptance and final publication gates, not across separate runs or retries.
Scope and configuration
Resources belong to the canonical original project, across all its refs, worktrees, and heph processes using the same data root. Symlink aliases coordinate. Distinct projects, clones, or data roots do not. A user-level rule applies independently in each project, not globally.
User and project maps merge by name: project patterns append to user patterns, and identical entries are deduplicated. An empty project array cannot remove a user requirement. Remembering an approval never modifies this table.
Heph reads the authoritative config files at startup. Restart to change lock policy. A selected set stays fixed while its operation waits and runs.
Waiting and ownership
Contention adds waiting for lock build-output to scrollback, naming the
resource that actually blocked acquisition and its last recorded owner when
available. Owner records include the conversation name and operation (including
job or gate identity). If the blocker or recorded owner changes, another wait
line appears. Metadata can be stale or briefly absent; it is not proof of
ownership.
The bottom status bar shows locks: worktree, build-output while those locks
are held. worktree is the session's worktree lease; publication appears
while it owns publication. Named execution resources stay listed through
cleanup, including background jobs after a turn ends. Long lists are shortened
with ...; scrollback retains full acquisition and release names and timings.
Publication contention also names the holding session, falling back to its PID
for older lock records, and scrollback records acquisition and release.
Waiting is interruptible and has no automatic timeout. Command and gate
deadlines begin after acquisition. Input, scrolling, and unrelated operations
remain responsive. Esc or Ctrl-C asks before aborting a queued or running
user command.
Resources remain held through command execution and supervised cleanup.
Background jobs keep their resources after returning a job ID. If a later call
waits on one, inspect or stop that job; /job-kill ID requests cancellation
without acquiring resources. A configured job_status or job_kill call whose
selected resources overlap its target job fails instead of waiting on itself.
Lock files live at locks/<project-hash>/<name>.lock under the heph data root.
They are persistent OS advisory locks: do not delete them to release ownership.
Owner metadata is diagnostic, potentially stale, and never proves a lock is
free. Acquisition failures refuse execution; there is no unlocked fallback.
A dedicated Linux supervisor owns locked commands and their descriptors. It terminates and reaps descendants before releasing resources, including when its heph owner crashes or drops the control channel. Forcibly killing the supervisor itself, or deliberately escaping supervision, is outside this guarantee. Detached writers are not supported; use a managed background job. Replacing the installed heph binary does not break supervisor startup in an existing session: supervisors continue using that session's running version. Restart heph to use the new version.
This is advisory coordination among heph operations, not a sandbox. Ordinary terminal commands do not participate. Serializing operations prevents overlap; it does not fix sequential cache-freshness errors or provide per-ref artifacts. There are no capacities, shared modes, global resources, or external wrapper.