HookStack
Back to catalogue
ContextSessionStart

Auto-pull on main at session start

Always start from a fresh, up-to-date main

At session start on main/master, silently fetches the remote and fast-forward pulls if the branch is behind. If the branch diverges, injects a warning instead of pulling. Ensures the agent always works from a fresh baseline.

What does the Auto-pull on main at session start hook do?

Auto-pull on main at session start is a Claude Code SessionStart hook. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. Always start from a fresh, up-to-date main.

As a SessionStart hook it runs after the action, reacting to what just happened rather than blocking it. Because it is a deterministic Node.js script, it executes on every matching event without relying on the model to remember — the guarantee that makes agentic workflows safe to automate.

Use cases

  • Always-fresh baseline
  • Shared branch synchronization
  • CI/CD pre-work sync
  • Prevent stale-code edits

Tags

#git#pull#sync#session#main-branch

settings.json fragment

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/session-start-pull-if-main.mjs",
            "type": "command"
          }
        ]
      }
    ]
  }
}

Script · .claude/hooks/session-start-pull-if-main.mjs

#!/usr/bin/env node
// @hookstack session-start-pull-if-main
// SessionStart: si on est sur main/master et qu'il y a des commits distants, lance git pull
import { execSync } from "node:child_process";
import { fileURLToPath } from "node:url";

function defaultExec(cmd) {
	try {
		return execSync(cmd, { encoding: "utf8", timeout: 10_000 }).trim();
	} catch {
		return "";
	}
}

function defaultPull() {
	execSync("git pull --ff-only --quiet", { encoding: "utf8", timeout: 30_000 });
}

export function run({ exec = defaultExec, pull = defaultPull } = {}) {
	const branch =
		exec("git branch --show-current") ||
		exec("git rev-parse --abbrev-ref HEAD");
	if (!branch || !/^(main|master)$/.test(branch)) return null;

	// Vérifier qu'un remote existe
	const remote = exec("git remote");
	if (!remote) return null;

	// Fetch silencieux pour connaître l'état du remote
	exec("git fetch --quiet 2>/dev/null");

	const localHash = exec("git rev-parse HEAD");
	const remoteHash = exec("git rev-parse @{u} 2>/dev/null");
	if (!remoteHash || localHash === remoteHash) return null;

	// Vérifier qu'on n'est pas en avance sur le remote (merge sûr)
	const behind = exec("git rev-list HEAD..@{u} --count");
	const ahead = exec("git rev-list @{u}..HEAD --count");
	if (parseInt(behind, 10) === 0) return null;

	if (parseInt(ahead, 10) > 0) {
		// Divergence — ne pas pull automatiquement
		return `${[
			`## ⚠️  Branche \`${branch}\` diverge du remote`,
			`- ${behind} commit(s) en retard, ${ahead} commit(s) en avance.`,
			`- Résolvez la divergence avant de continuer (\`git pull --rebase\` ou merge manuel).`,
		].join("\n")}\n`;
	}

	// Pull sans divergence
	try {
		pull();
	} catch {
		return `## ⚠️  \`git pull\` a échoué sur \`${branch}\` — synchronisez manuellement.\n`;
	}

	return `${[
		`## Dépôt synchronisé`,
		`- \`${behind}\` commit(s) récupéré(s) depuis le remote sur \`${branch}\`.`,
	].join("\n")}\n`;
}

/* v8 ignore next 4 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
	const result = run();
	if (result) process.stdout.write(result);
}

Learn more

Related hooks