> ## Documentation Index
> Fetch the complete documentation index at: https://bilanc.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# MDM Deployment

> Roll posthook out across your organisation with Jamf, Kandji, Intune, or any MDM that can run a shell script.

Deploy posthook to every engineer's machine from your MDM instead of asking each person to run the install command. This is the recommended path for org-wide rollouts: engineers do nothing, hooks are in place before their next agent session, and new machines are covered as they're enrolled.

The MDM flow uses the same installer as the [share-a-link install](/posthook/overview#share-a-link). The only differences are that the script runs as the logged-in user from a management agent, and you pass the engineer's identity in through an environment variable because there's no terminal to prompt on.

<Note>
  Posthook supports **macOS and Linux** (Intel and arm64). Windows isn't supported yet, so skip Windows devices in your scope.
</Note>

## Before you start

<Steps>
  <Step title="Get a team install key">
    In Bilanc, a **Owner or Manager** goes to **Admin → Connections → AI Tools → PostHook** and clicks **Generate install link**. The command shown contains the key after `apiKey=`:

    ```bash theme={null}
    curl -fsSL "https://api.bilanc.co/posthook/install.sh?apiKey=<team-key>" | sh
    ```

    Copy the whole URL. The key is shown once; store it in your MDM's secrets store or script variables, not in a wiki.

    The key is org-scoped and **write-only** — it can send posthook data into your workspace and cannot read anything. One key serves the whole organisation.
  </Step>

  <Step title="Decide how you'll pass the engineer's email">
    Posthook stamps every session with a work email so the dashboard can attribute activity to a person. Interactively, the installer asks for it. Under MDM there is no terminal, so set `POSTHOOK_ENGINEER_EMAIL` in the script instead.

    Most MDM tools expose the assigned user's email as a script variable — use that. If yours doesn't, you can leave it out: sessions fall back to the machine's global `git config user.email`, and the engineer can set it later with `posthook identity --set-email you@company.com`.
  </Step>

  <Step title="Allow the network egress">
    The installer and the sync daemon need HTTPS to:

    | Host                                          | Used for                                        |
    | --------------------------------------------- | ----------------------------------------------- |
    | `api.bilanc.co`                               | serving the installer and receiving synced data |
    | `raw.githubusercontent.com`                   | the open-source install script                  |
    | `api.github.com`                              | resolving the latest release version            |
    | `github.com`, `objects.githubusercontent.com` | downloading the release binary and checksums    |

    Self-hosted Bilanc customers replace `api.bilanc.co` with their own API host. The install link your dashboard generates already points at the right place.
  </Step>

  <Step title="Install agents before posthook, or re-run init after">
    `posthook init` installs hooks only for agents it finds on the machine (it looks for `~/.claude`, `~/.cursor`, and `~/.codex`). If Claude Code, Cursor, or Codex is installed later, run `posthook init` again — it's idempotent. The simplest way to cover this is to make the install script a **recurring** policy (see [Keep it current](#keep-it-current)).
  </Step>
</Steps>

## Install script

The install must run **as the logged-in user, not root**. Everything posthook touches lives in the user's home directory: the binary in `~/.local/bin`, the database and config in `~/.posthook`, the agent hook files, and the per-user launchd agent or systemd unit. Running it as root installs posthook for root and captures nothing.

Replace `<team-key>` with your key and `<engineer-email>` with your MDM's user-email variable. Both are set as plain shell variables at the top of the script, so MDM parameter substitution (Jamf `$4`-style parameters, Kandji or Intune variables) works as normal.

<Tabs>
  <Tab title="macOS">
    MDM scripts on macOS run as root, so switch to the console user first. This is the same pattern Jamf, Kandji, and Mosyle recommend for per-user installs.

    ```bash theme={null}
    #!/bin/bash
    set -e

    INSTALL_URL="https://api.bilanc.co/posthook/install.sh?apiKey=<team-key>"
    ENGINEER_EMAIL="<engineer-email>"   # e.g. your MDM's user-email variable

    CONSOLE_USER="$(/usr/bin/stat -f%Su /dev/console)"
    if [ -z "$CONSOLE_USER" ] || [ "$CONSOLE_USER" = "root" ] || [ "$CONSOLE_USER" = "loginwindow" ]; then
      echo "No user logged in — skipping posthook install." >&2
      exit 0
    fi

    su - "$CONSOLE_USER" -c "POSTHOOK_ENGINEER_EMAIL='$ENGINEER_EMAIL' sh -c 'curl -fsSL \"$INSTALL_URL\" | sh'"
    ```

    If no one is logged in the script exits cleanly so your MDM retries on the next check-in rather than marking the device failed.
  </Tab>

  <Tab title="Linux">
    Run as the target user. If your management agent runs as root, drop privileges with `runuser` (or `su -`):

    ```bash theme={null}
    #!/bin/bash
    set -e

    INSTALL_URL="https://api.bilanc.co/posthook/install.sh?apiKey=<team-key>"
    ENGINEER_EMAIL="<engineer-email>"
    TARGET_USER="<username>"

    runuser -l "$TARGET_USER" -c "POSTHOOK_ENGINEER_EMAIL='$ENGINEER_EMAIL' sh -c 'curl -fsSL \"$INSTALL_URL\" | sh'"
    ```

    The sync daemon is a `systemd --user` unit. The installer runs `loginctl enable-linger` so it survives logout; on systems where that needs elevated rights, run `loginctl enable-linger <username>` as root once.
  </Tab>
</Tabs>

### What the script does

1. Resolves the latest release from GitHub (or the version pinned in `POSTHOOK_VERSION`).
2. Downloads the binary for the machine's OS and CPU, verifies its SHA-256 checksum, and installs it to `~/.local/bin/posthook`.
3. Downloads the local dashboard bundle to `~/.posthook/dash` (optional feature; skipped silently if unavailable).
4. Writes the team key and endpoint into `~/.posthook/config.json` and enables sync.
5. Runs `posthook init`: installs hooks for every detected agent, installs the `git` shadow symlink next to the binary, and writes a global git template as a fallback.
6. Records the engineer identity from `POSTHOOK_ENGINEER_EMAIL` without prompting.
7. Installs and starts the background sync daemon: a launchd user agent named `co.bilanc.posthook-sync` on macOS, or a `posthook-sync.service` systemd user unit on Linux. It starts at login, restarts on failure, and flushes new rows every 5 seconds.

Re-running the script is safe. It upgrades the binary in place, re-runs `init`, keeps the existing identity and database, and restarts the daemon so the new binary takes over.

### Environment variables

Set these inside the user context, before the `curl … | sh` line.

| Variable                  | Required    | Purpose                                                                     |
| ------------------------- | ----------- | --------------------------------------------------------------------------- |
| `POSTHOOK_ENGINEER_EMAIL` | Recommended | Work email sessions are attributed to. Skips the interactive prompt.        |
| `POSTHOOK_ENGINEER_NAME`  | No          | Display name to go with the email. Defaults to the git `user.name`.         |
| `POSTHOOK_VERSION`        | No          | Pin a release, e.g. `0.2.10`. Default is latest. Pin if you stage rollouts. |
| `POSTHOOK_INSTALL_DIR`    | No          | Where the binary and `git` shadow go. Default `~/.local/bin`.               |

`POSTHOOK_API_KEY` and `POSTHOOK_CLOUD_ENDPOINT` are set for you by the install link; you don't need to set them yourself.

## PATH and the git shadow

Commit capture works through a `git` symlink that the installer places next to the posthook binary. For it to see every commit, `~/.local/bin` must come **before** the real `git` on `PATH` in the shells and IDE terminals engineers actually use. The installer checks this and prints a warning if it isn't, but it never edits shell profiles.

For an MDM rollout, add the path once for everyone:

<Tabs>
  <Tab title="macOS">
    Append to `/etc/zshenv` (read by every zsh, including non-interactive shells spawned by IDEs and agents):

    ```bash theme={null}
    export PATH="$HOME/.local/bin:$PATH"
    ```
  </Tab>

  <Tab title="Linux">
    Create `/etc/profile.d/posthook.sh`:

    ```bash theme={null}
    export PATH="$HOME/.local/bin:$PATH"
    ```
  </Tab>
</Tabs>

If you can't change `PATH` on locked-down machines, capture still works for repositories cloned or created **after** install, via the global git template posthook sets up. Existing checkouts need `posthook track <path>` run once each.

<Tip>
  Homebrew users often have `/opt/homebrew/bin` prepended in their own shell profile, ahead of anything set in `/etc/zshenv`. `posthook status` reports whether the shadow is winning on that machine.
</Tip>

## Verify the rollout

On any machine, as the engineer:

```bash theme={null}
posthook status            # hooks installed, shadow healthy, recent captures
posthook identity          # which email sessions are attributed to
posthook service status    # daemon installed and running
posthook sync --status     # pending rows and last successful flush
```

In Bilanc, open the **Posthook** page. Machines appear as sessions arrive, usually within a minute of an engineer's first agent interaction after install. Sessions with no identity show under the engineer's git email until `posthook identity` is set.

Logs for the daemon are in `~/.posthook/sync.log`.

## Keep it current

Posthook does not self-update. To keep machines on the latest release, either:

* **Re-run the install script on a schedule** from your MDM (weekly is plenty). The same script upgrades in place and re-detects any newly installed agents.
* Or have engineers run `posthook update`, which does the same thing locally.

Pin `POSTHOOK_VERSION` if you want to control exactly which build ships, and bump it deliberately.

## Offboarding a machine

Run as the user:

```bash theme={null}
posthook service uninstall                  # stop and remove the daemon
rm -f ~/.local/bin/posthook ~/.local/bin/git
rm -rf ~/.posthook
```

Then remove the posthook hook entries from `~/.claude/settings.json`, `~/.cursor/hooks.json`, and `~/.codex/config.toml` if those agents remain in use, so they don't call a binary that's no longer there.

To cut off sync for **every** machine at once — for example if the key leaks — revoke the install link from **Admin → Connections → PostHook** in Bilanc and redeploy with a freshly generated one.

## Troubleshooting

<AccordionGroup>
  <Accordion title="Installed, but nothing appears in Bilanc">
    Check in order: `posthook sync --status` for a recent successful flush and no auth error; `posthook service status` to confirm the daemon is loaded; `posthook status` to confirm hooks were installed for the agents the engineer uses. If the agent shows as *not detected*, it was installed after posthook — run `posthook init`.
  </Accordion>

  <Accordion title="Sessions are attributed to the wrong email or to no one">
    The identity wasn't set at install (no `POSTHOOK_ENGINEER_EMAIL`) and the machine's git email is personal or empty. Fix on the machine with `posthook identity --set-email you@company.com`. Posthook retro-stamps that machine's existing unattributed sessions when you do.
  </Accordion>

  <Accordion title="Commits are missing but agent sessions show up">
    The `git` shadow isn't first on `PATH` for the shell the engineer commits from. See [PATH and the git shadow](#path-and-the-git-shadow), or run `posthook track <path>` on the affected repositories.
  </Accordion>

  <Accordion title="The daemon doesn't start on macOS">
    The install ran as root or from a context with no GUI session, so the launchd agent couldn't be bootstrapped into the user's session. Re-run the install script while the user is logged in. `launchctl print gui/$(id -u)/co.bilanc.posthook-sync` shows the agent's state.
  </Accordion>

  <Accordion title="Machines behind an HTTP proxy">
    The installer's `curl` calls honour `HTTPS_PROXY`. The sync daemon is launched by launchd or systemd and does **not** inherit shell proxy variables, so egress to `api.bilanc.co` must be allowed directly. If your network requires a proxy for all outbound traffic, contact [support](mailto:sam@bilanc.co) before rolling out.
  </Accordion>
</AccordionGroup>
