# How Caveman Installation Hooks Manage File Ownership and CI Synchronization

> Learn how Caveman installation hooks ensure file-ownership safety and synchronize CI workflows. Discover atomic writes, UID verification, and automatic source file mirroring for seamless management.

- Repository: [Julius Brussee/caveman](https://github.com/JuliusBrussee/caveman)
- Tags: how-to-guide
- Published: 2026-07-07

---

**Caveman installation hooks enforce file-ownership safety through atomic writes with symlink protection and UID verification in [`src/hooks/caveman-config.js`](https://github.com/JuliusBrussee/caveman/blob/main/src/hooks/caveman-config.js), while the [`.github/workflows/sync-skill.yml`](https://github.com/JuliusBrussee/caveman/blob/main/.github/workflows/sync-skill.yml) CI workflow automatically mirrors authoritative source files from `src/hooks/` into the `plugins/caveman/` distribution to keep installations synchronized across all agents.**

The Caveman project provides a defensive hook system for Claude Code that requires rigorous file permission controls and automated distribution management. Understanding how Caveman installation hooks manage file ownership and CI synchronization reveals an architecture designed to prevent privilege escalation attacks while maintaining consistency across development environments.

## Defensive File-Ownership Checks in the Hook Installer

The installation process centers on three hook files ([`caveman-activate.js`](https://github.com/JuliusBrussee/caveman/blob/main/caveman-activate.js), [`caveman-mode-tracker.js`](https://github.com/JuliusBrussee/caveman/blob/main/caveman-mode-tracker.js), and the status-line badge) deployed via [`src/hooks/install.sh`](https://github.com/JuliusBrussee/caveman/blob/main/src/hooks/install.sh). Every write operation incorporates ownership verification to prevent cross-user attacks.

### Atomic, Symlink-Safe Writes with safeWriteFlag()

The `safeWriteFlag()` function in [`src/hooks/caveman-config.js`](https://github.com/JuliusBrussee/caveman/blob/main/src/hooks/caveman-config.js) (lines 31-41) implements atomic file creation to prevent race condition exploits. The function creates temporary files using `O_NOFOLLOW | O_EXCL` flags, ensuring the system throws an error if a symlink exists at the target location. It then commits the write via `fs.renameSync`, which is an atomic operation on POSIX systems. This eliminates the window where an attacker could swap a file for a malicious symlink between creation and write operations.

### Cross-Platform UID Verification

Before writing the mode flag to `$CLAUDE_CONFIG_DIR/.caveman-active`, the code resolves the parent directory using `fs.realpathSync` and compares the directory's UID to the current process UID (lines 43-56). If the UIDs differ, the function aborts immediately, preventing a cross-user symlink attack where a malicious user might own the directory path. On Windows systems where UID checks are unavailable, the implementation falls back to path traversal verification, ensuring the resolved directory lives within the user's home tree and rejecting any path that escapes it (lines 57-65).

### Idempotent Installation Flow

The [`src/hooks/install.sh`](https://github.com/JuliusBrussee/caveman/blob/main/src/hooks/install.sh) script implements idempotent checks (lines 48-86) that verify hook file presence and verify they are properly wired into `~/.claude/settings.json` before making changes. If the script detects that `ALL_FILES_PRESENT` and `HOOKS_WIRED` conditions are satisfied, it exits early unless the `--force` flag is supplied. This guarantees that subsequent CI-driven updates will not produce duplicate entries in the user's configuration.

## Automated CI Synchronization of Hook Files

Caveman maintains a strict separation between source-of-truth files and distribution artifacts. The authoritative hook logic lives in `src/hooks/`, while the runtime distribution resides in `plugins/caveman/`.

### The sync-skill.yml Workflow

The GitHub Actions workflow defined in [`.github/workflows/sync-skill.yml`](https://github.com/JuliusBrussee/caveman/blob/main/.github/workflows/sync-skill.yml) monitors the `main` branch for changes to `skills/**/*.SKILL.md` and `agents/cavecrew-*.md`. When changes are detected, the workflow triggers automatically to rebuild the plugin distribution. The workflow commits generated mirrors back to the repository with a `[skip ci]` message to prevent infinite automation loops.

### Mirroring Source-of-Truth to Plugin Distribution

The CI workflow performs three critical synchronization steps:

1. Copies canonical skill markdown files from `skills/caveman/` into `plugins/caveman/skills/`
2. Copies the `caveman-compress` skill and auxiliary scripts into the plugin tree
3. Copies `agents/cavecrew-*.md` files into `plugins/caveman/agents/`
4. Rebuilds the release ZIP at `dist/caveman.skill`

Because the installer only appends missing entries to [`settings.json`](https://github.com/JuliusBrussee/caveman/blob/main/settings.json) and verifies file presence before copying, these CI-generated mirrors can safely overwrite stale hook files without destroying user customizations or triggering duplicate installations.

## Integration: How Ownership Safety and CI Sync Work Together

The defensive design ensures that fresh installs and CI updates coexist safely. When a user runs the installer, it copies hook files into `~/.claude/hooks/` and writes the mode flag using `safeWriteFlag()`, guaranteeing ownership by the current user. Later, when CI updates the `plugins/caveman/` mirror, the same hook files are updated in the distribution. Because the installer's idempotent checks detect existing installations, users can safely pull CI updates without re-running the installer, while new users receive the latest synchronized version automatically.

```bash

# Install the hooks (idempotent operation)

$ bash src/hooks/install.sh

# Force reinstall to overwrite existing files

$ bash src/hooks/install.sh --force

# Verify ownership of the flag file after install

$ ls -l ~/.claude/.caveman-active
-rw------- 1 $(whoami) $(whoami) 4 Sep 12 14:23 /home/you/.claude/.caveman-active

```

```json
// SessionStart entry added by install.sh to ~/.claude/settings.json
{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node \"${HOME}/.claude/hooks/caveman-activate.js\"",
            "timeout": 5,
            "statusMessage": "Loading caveman mode..."
          }
        ]
      }
    ]
  }
}

```

```yaml

# Excerpt from .github/workflows/sync-skill.yml

on:
  push:
    branches: [ main ]
    paths:
      - 'skills/**/SKILL.md'
      - 'agents/cavecrew-*.md'

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Mirror skills to plugin
        run: |
          cp -r skills/caveman plugins/caveman/skills/
          cp agents/cavecrew-*.md plugins/caveman/agents/
      - name: Rebuild ZIP
        run: node bin/install.js
      - name: Commit mirrors
        run: |
          git add plugins/caveman
          git commit -m "Sync plugin mirrors [skip ci]"
          git push

```

## Summary

- **Atomic writes prevent symlink attacks**: The `safeWriteFlag()` function in [`src/hooks/caveman-config.js`](https://github.com/JuliusBrussee/caveman/blob/main/src/hooks/caveman-config.js) uses `O_NOFOLLOW | O_EXCL` and atomic renames to eliminate race conditions during file creation.
- **UID verification enforces ownership**: The code compares directory UIDs to the process UID before writing to `$CLAUDE_CONFIG_DIR/.caveman-active`, with Windows fallbacks to home-directory path validation.
- **CI automation maintains distribution**: The [`.github/workflows/sync-skill.yml`](https://github.com/JuliusBrussee/caveman/blob/main/.github/workflows/sync-skill.yml) workflow mirrors `src/hooks/` and skill files into `plugins/caveman/` automatically on every push to `main`.
- **Idempotent installation prevents conflicts**: The [`src/hooks/install.sh`](https://github.com/JuliusBrussee/caveman/blob/main/src/hooks/install.sh) script checks for existing installations before modifying [`settings.json`](https://github.com/JuliusBrussee/caveman/blob/main/settings.json), allowing CI updates to overwrite distribution files without duplicating user configuration entries.

## Frequently Asked Questions

### How does Caveman prevent symlink attacks during installation?

Caveman prevents symlink attacks through the `safeWriteFlag()` function in [`src/hooks/caveman-config.js`](https://github.com/JuliusBrussee/caveman/blob/main/src/hooks/caveman-config.js), which creates temporary files with `O_NOFOLLOW | O_EXCL` flags before renaming them atomically. The function also resolves the parent directory using `fs.realpathSync` and verifies the directory UID matches the current process UID before any write operations occur.

### What happens if the CI workflow overwrites my local hook files?

The CI workflow updates the `plugins/caveman/` distribution directory, not your local `~/.claude/hooks/` installation. If you installed hooks via [`src/hooks/install.sh`](https://github.com/JuliusBrussee/caveman/blob/main/src/hooks/install.sh), the script's idempotent checks detect existing files and skip duplication. You can safely pull CI-updated distributions without affecting your active hook configuration in [`settings.json`](https://github.com/JuliusBrussee/caveman/blob/main/settings.json).

### Why does the installer check file ownership before writing the flag?

The ownership check prevents privilege escalation attacks where a malicious user might create a symlink in `$CLAUDE_CONFIG_DIR` pointing to a sensitive file owned by another user. By verifying that the target directory's UID matches the current process UID (or on Windows, that the path remains within the home directory), Caveman ensures it only writes to directories the user actually controls.

### How can I verify that my Caveman installation is synchronized with the latest CI build?

Check that your local hook files in `~/.claude/hooks/` match the source files in `src/hooks/` (specifically [`caveman-config.js`](https://github.com/JuliusBrussee/caveman/blob/main/caveman-config.js), [`caveman-activate.js`](https://github.com/JuliusBrussee/caveman/blob/main/caveman-activate.js), and [`caveman-mode-tracker.js`](https://github.com/JuliusBrussee/caveman/blob/main/caveman-mode-tracker.js)). The CI workflow keeps `plugins/caveman/` synchronized with these sources, so running `bash src/hooks/install.sh --force` will reinstall from the latest distribution if you need to resynchronize manually.