feat: add fallback periodic sync via launchd/systemd

What does this MR do?

Adds a fallback periodic sync so agent sessions still reach GitLab when the hooks fail to sync them (no network, glab crash, agent killed before the hook runs).

What glab govern setup installs

In addition to the existing Claude Code hooks, glab govern setup installs a scheduled job by default:

Platform Job Files Schedule
macOS launchd agent ~/Library/LaunchAgents/com.gitlab.glab-govern-audit-sync.plist every 30 minutes
Linux systemd user timer ~/.config/systemd/user/glab-govern-audit-sync.{service,timer} every 30 minutes, and 5 minutes after boot
Windows not supported – –
  • The confirmation prompt and --help name the job. --no-fallback-sync installs only the hooks.
  • glab govern setup --uninstall removes the job. It leaves the hooks in place.
  • The job runs the glab found on PATH at setup time (for example /opt/homebrew/bin/glab or a mise shim), as long as it reports the same version and commit as the glab running setup. Otherwise it runs the glab running setup. Symlinks and shims aren't resolved, so package-manager upgrades don't break it.
  • The job doesn't depend on the current Git repository, so setup installs it from any directory.
  • The job runs with GLAB_CONFIG_DIR set to the glab configuration directory in use at setup time. launchd and systemd don't pass on the user's shell environment, so without this a custom GLAB_CONFIG_DIR or XDG_CONFIG_HOME would point the job at a different directory from the hooks.

What gets uploaded, and where

The job runs glab govern audit sync --all:

  • Which sessions: only sessions the hooks have already recorded. On each run the hook writes a .meta file under ~/.config/glab-cli/gaig/cursors/ with the agent type, project, host, and how to find the transcript. Sessions the hooks never saw are never uploaded.
  • Where: each session goes to the project and host recorded by its hook (the Git remote of the directory the agent ran in), using the stored glab credentials for that host. --all can't be combined with -R.
  • What: the same audit events the hooks send (tool name and timestamp per tool call), plus the session goal.
  • Unattended: the job runs with no agent or terminal open.
  • Pausing: Claude Code sessions are skipped while the glab Stop hook is missing from ~/.claude/settings.json, so removing the hooks pauses them. Sessions from other agents keep syncing.

Other new behavior

  • Agents: session sync goes through a small agent interface (detect the session, open its transcript, reopen it later from .meta, report whether syncing is paused). Claude Code and OpenCode implement it. Adding an agent is one new type.
  • OpenCode: sessions are read with opencode export <id>. This also fixes the hook path, which used to look for OpenCode transcripts under ~/.claude/projects. glab doesn't read OpenCode's SQLite storage directly, so no new dependency is added.
  • glab govern audit sync --all:
    • Reads each transcript before making any API call, and skips sessions with nothing new.
    • Looks up each project once per run.
    • Marks a session completed after 24 hours of inactivity, in case the SessionEnd hook never ran. Completed sessions are skipped on later runs.
    • Deletes the local cursor and .meta of a session whose transcript the agent has deleted, so later runs don't keep looking for it.
  • Run log: each --all run writes ~/.config/glab-cli/gaig/fallback-sync-status.json with its time, counts, paused agents, and up to 20 errors.
  • glab govern doctor has a new "Fallback periodic sync" check that shows:
    • whether the job is installed, and loaded in launchd or systemd;
    • whether the glab binary it runs still exists (fails with a fix if not);
    • whether its last run exited with an error, which catches runs that fail before they can write the run log;
    • the last run's results, with a warning if no run happened for more than an hour.

Limitations

  • Windows is not supported: the job is not installed, and doctor says so.
  • Only sessions with a .meta file are synced. setup installs hooks only for Claude Code, so OpenCode sessions are synced only if something calls glab govern audit sync with OPENCODE_SESSION_ID set.
  • Local state for completed sessions is kept: each leaves a small cursor file and a .meta file in ~/.config/glab-cli/gaig/cursors/, which later runs skip without any API calls. It's removed once the agent deletes the transcript.
  • OpenCode syncing can't be paused by removing its integration, because glab doesn't install or manage it, so --all has nothing to check. glab govern setup --uninstall stops the job for every agent. Once setup installs an OpenCode integration, OpenCode can check for it the same way Claude Code checks its hooks.
  • With mise or asdf, the job runs the shim. launchd and systemd start the job in /, so the shim uses the global glab version. If glab is only pinned in a project's .tool-versions, the job fails to start, and doctor reports its non-zero exit.
  • OpenCode before 1.2 stored sessions per project, so opencode export run from the job may not find them. Current versions look sessions up by ID in a single database.
  • If a hook and the job sync the same session at the same moment, both can post the same events. Event IDs are deterministic, so whether duplicates appear depends on GitLab deduplicating them.
  • On Linux, if the token is stored in the system keyring and no desktop session is running, the job may not be able to read it. doctor shows the resulting errors.
  • An OpenCode session's cursor is a count of tool calls, so tool calls made after a revert are not synced until the count passes the old cursor.
  • Idle completion uses a fixed 24-hour threshold. If a session resumes after being auto-completed, its later events go to the completed GitLab session.

Who is affected

Only users who run glab govern setup (an experimental command). Nobody else sees any change.

How to remove it

  • The job: glab govern setup --uninstall, or manually:
    • macOS: launchctl bootout gui/$(id -u)/com.gitlab.glab-govern-audit-sync && rm ~/Library/LaunchAgents/com.gitlab.glab-govern-audit-sync.plist
    • Linux: systemctl --user disable --now glab-govern-audit-sync.timer && rm ~/.config/systemd/user/glab-govern-audit-sync.{service,timer} && systemctl --user daemon-reload
  • The hooks: delete the glab govern audit sync entries from ~/.claude/settings.json.
Edited by Jean van der Walt

Merge request reports

Loading
Loading