Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 57 additions & 14 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
set -euo pipefail

branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
if [[ -z "$branch" || "$branch" == "HEAD" ]]; then
branch="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
fi
if [[ -z "$branch" ]]; then
exit 0
fi
Expand All @@ -10,18 +13,15 @@ if [[ "${ALLOW_COMMIT_ON_PROTECTED_BRANCH:-0}" == "1" ]]; then
exit 0
fi

is_vscode_git_context=0
if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n "${VSCODE_IPC_HOOK_CLI:-}" ]]; then
is_vscode_git_context=1
is_unborn_branch=0
if ! git rev-parse --verify HEAD >/dev/null 2>&1; then
is_unborn_branch=1
fi

allow_vscode_protected_branch_raw="${MUSAFETY_ALLOW_VSCODE_PROTECTED_BRANCH:-$(git config --get multiagent.protectedBranches.allowVSCode || true)}"
allow_vscode_protected_branch_raw="$(printf '%s' "${allow_vscode_protected_branch_raw:-}" | tr '[:upper:]' '[:lower:]')"
allow_vscode_protected_branch=0
case "$allow_vscode_protected_branch_raw" in
1|true|yes|on) allow_vscode_protected_branch=1 ;;
*) allow_vscode_protected_branch=0 ;;
esac
is_codex_session=0
if [[ -n "${CODEX_THREAD_ID:-}" || -n "${OMX_SESSION_ID:-}" || "${CODEX_CI:-0}" == "1" ]]; then
is_codex_session=1
fi

protected_branches_raw="${MUSAFETY_PROTECTED_BRANCHES:-$(git config --get multiagent.protectedBranches || true)}"
if [[ -z "$protected_branches_raw" ]]; then
Expand All @@ -37,8 +37,54 @@ for protected_branch in $protected_branches_raw; do
fi
done

codex_require_agent_branch_raw="${MUSAFETY_CODEX_REQUIRE_AGENT_BRANCH:-$(git config --get multiagent.codexRequireAgentBranch || true)}"
if [[ -z "$codex_require_agent_branch_raw" ]]; then
codex_require_agent_branch_raw="true"
fi
codex_require_agent_branch="$(printf '%s' "$codex_require_agent_branch_raw" | tr '[:upper:]' '[:lower:]')"

should_require_codex_agent_branch=0
case "$codex_require_agent_branch" in
1|true|yes|on) should_require_codex_agent_branch=1 ;;
0|false|no|off) should_require_codex_agent_branch=0 ;;
*) should_require_codex_agent_branch=1 ;;
esac

if [[ "$should_require_codex_agent_branch" == "1" && "${MUSAFETY_ALLOW_CODEX_ON_NON_AGENT:-0}" != "1" ]]; then
if [[ "$is_codex_session" == "1" && "$branch" != agent/* ]]; then
if [[ "$is_protected_branch" == "1" ]]; then
cat >&2 <<'MSG'
[guardex-preedit-guard] Codex edit/commit detected on a protected branch.
GuardeX requires Codex work to run from an isolated agent/* branch.
Start the sub-branch/worktree with:
bash scripts/codex-agent.sh "<task-or-plan>" "<agent-name>"
Or manually:
bash scripts/agent-branch-start.sh "<task-or-plan>" "<agent-name>"
Then commit from the created agent/* branch.

Temporary bypass (not recommended):
MUSAFETY_ALLOW_CODEX_ON_NON_AGENT=1 git commit ...
MSG
exit 1
fi

cat >&2 <<'MSG'
[codex-branch-guard] Codex agent commit blocked on non-agent branch.
Use isolated branch/worktree first:
bash scripts/agent-branch-start.sh "<task-or-plan>" "<agent-name>"
Then commit from the created agent/* branch.

Temporary bypass (not recommended):
MUSAFETY_ALLOW_CODEX_ON_NON_AGENT=1 git commit ...
Disable this rule for a repo (not recommended):
git config multiagent.codexRequireAgentBranch false
MSG
exit 1
fi
fi

if [[ "$is_protected_branch" == "1" ]]; then
if [[ "$is_vscode_git_context" == "1" && "$allow_vscode_protected_branch" == "1" ]]; then
if [[ "$is_unborn_branch" == "1" && "$is_codex_session" != "1" ]]; then
exit 0
fi

Expand All @@ -54,9 +100,6 @@ Use an agent branch first:
After finishing work:
bash scripts/agent-branch-finish.sh

Optional repo override to allow VS Code Source Control commits:
git config multiagent.protectedBranches.allowVSCode true

Temporary bypass (not recommended):
ALLOW_COMMIT_ON_PROTECTED_BRANCH=1 git commit ...
MSG
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,13 @@ By default this writes:

#### Real VS Code Source Control example (after `gx setup`)

![GuardeX real VS Code Source Control layout](./docs/images/workflow-vscode-guardex-real.png)

This is the exact layout you should expect in VS Code Source Control after setup
and a few `agent-branch-start` runs:

```text
multiagent-safety (main)
GuardeX (your preferred local branch: main/dev)
agent_codex_<timestamp>-<snapshot>-<task>
agent_bot_<timestamp>-<snapshot>-<task>
agent_bot_<timestamp>-<snapshot>-<task>
Expand Down Expand Up @@ -347,6 +349,7 @@ multiagent.protectedBranches
- direct commits to protected branches (defaults: `dev`, `main`, `master`; configurable via `gx protect ...`)
- protected-branch commits are blocked regardless of commit client (including VS Code Source Control)
- Codex-session commits on non-`agent/*` branches are blocked by default (`multiagent.codexRequireAgentBranch=true`)
- Codex commits attempted on protected branches trigger `guardex-preedit-guard` and require starting work via `scripts/codex-agent.sh`
- overlapping file ownership between agents
- unapproved deletions of claimed files
- risky stale/missing lock state
Expand Down
Binary file added docs/images/workflow-vscode-guardex-real.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
76 changes: 52 additions & 24 deletions templates/githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
set -euo pipefail

branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
if [[ -z "$branch" || "$branch" == "HEAD" ]]; then
branch="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
fi
if [[ -z "$branch" ]]; then
exit 0
fi
Expand All @@ -10,6 +13,16 @@ if [[ "${ALLOW_COMMIT_ON_PROTECTED_BRANCH:-0}" == "1" ]]; then
exit 0
fi

is_unborn_branch=0
if ! git rev-parse --verify HEAD >/dev/null 2>&1; then
is_unborn_branch=1
fi

is_codex_session=0
if [[ -n "${CODEX_THREAD_ID:-}" || -n "${OMX_SESSION_ID:-}" || "${CODEX_CI:-0}" == "1" ]]; then
is_codex_session=1
fi

protected_branches_raw="${MUSAFETY_PROTECTED_BRANCHES:-$(git config --get multiagent.protectedBranches || true)}"
if [[ -z "$protected_branches_raw" ]]; then
protected_branches_raw="dev main master"
Expand All @@ -24,25 +37,6 @@ for protected_branch in $protected_branches_raw; do
fi
done

if [[ "$is_protected_branch" == "1" ]]; then
git_dir="$(git rev-parse --git-dir)"
if [[ -f "$git_dir/MERGE_HEAD" ]]; then
exit 0
fi

cat >&2 <<'MSG'
[agent-branch-guard] Direct commits on protected branches are blocked.
Use an agent branch first:
bash scripts/agent-branch-start.sh "<task-or-plan>" "<agent-name>"
After finishing work:
bash scripts/agent-branch-finish.sh

Temporary bypass (not recommended):
ALLOW_COMMIT_ON_PROTECTED_BRANCH=1 git commit ...
MSG
exit 1
fi

codex_require_agent_branch_raw="${MUSAFETY_CODEX_REQUIRE_AGENT_BRANCH:-$(git config --get multiagent.codexRequireAgentBranch || true)}"
if [[ -z "$codex_require_agent_branch_raw" ]]; then
codex_require_agent_branch_raw="true"
Expand All @@ -57,12 +51,23 @@ case "$codex_require_agent_branch" in
esac

if [[ "$should_require_codex_agent_branch" == "1" && "${MUSAFETY_ALLOW_CODEX_ON_NON_AGENT:-0}" != "1" ]]; then
is_codex_session=0
if [[ -n "${CODEX_THREAD_ID:-}" || -n "${OMX_SESSION_ID:-}" || "${CODEX_CI:-0}" == "1" ]]; then
is_codex_session=1
fi

if [[ "$is_codex_session" == "1" && "$branch" != agent/* ]]; then
if [[ "$is_protected_branch" == "1" ]]; then
cat >&2 <<'MSG'
[guardex-preedit-guard] Codex edit/commit detected on a protected branch.
GuardeX requires Codex work to run from an isolated agent/* branch.
Start the sub-branch/worktree with:
bash scripts/codex-agent.sh "<task-or-plan>" "<agent-name>"
Or manually:
bash scripts/agent-branch-start.sh "<task-or-plan>" "<agent-name>"
Then commit from the created agent/* branch.

Temporary bypass (not recommended):
MUSAFETY_ALLOW_CODEX_ON_NON_AGENT=1 git commit ...
MSG
exit 1
fi

cat >&2 <<'MSG'
[codex-branch-guard] Codex agent commit blocked on non-agent branch.
Use isolated branch/worktree first:
Expand All @@ -78,6 +83,29 @@ MSG
fi
fi

if [[ "$is_protected_branch" == "1" ]]; then
if [[ "$is_unborn_branch" == "1" && "$is_codex_session" != "1" ]]; then
exit 0
fi

git_dir="$(git rev-parse --git-dir)"
if [[ -f "$git_dir/MERGE_HEAD" ]]; then
exit 0
fi

cat >&2 <<'MSG'
[agent-branch-guard] Direct commits on protected branches are blocked.
Use an agent branch first:
bash scripts/agent-branch-start.sh "<task-or-plan>" "<agent-name>"
After finishing work:
bash scripts/agent-branch-finish.sh

Temporary bypass (not recommended):
ALLOW_COMMIT_ON_PROTECTED_BRANCH=1 git commit ...
MSG
exit 1
fi

if [[ "$branch" == agent/* ]]; then
if ! python3 scripts/agent-file-locks.py validate --branch "$branch" --staged; then
cat >&2 <<'MSG'
Expand Down
16 changes: 16 additions & 0 deletions test/install.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,22 @@ test('setup pre-commit blocks codex session commits on non-agent branches by def
assert.match(result.stderr, /\[codex-branch-guard\] Codex agent commit blocked on non-agent branch\./);
});

test('setup pre-commit detects codex commit attempts on protected main and requires GuardeX sub-branch', () => {
const repoDir = initRepoOnBranch('main');

let result = runNode(['setup', '--target', repoDir], repoDir);
assert.equal(result.status, 0, result.stderr || result.stdout);

fs.writeFileSync(path.join(repoDir, 'notes-main.txt'), 'hello from main\n', 'utf8');
result = runCmd('git', ['add', 'notes-main.txt'], repoDir);
assert.equal(result.status, 0, result.stderr || result.stdout);

result = runCmd('git', ['commit', '-m', 'codex protected commit'], repoDir, { CODEX_THREAD_ID: 'test-thread' });
assert.notEqual(result.status, 0, result.stdout);
assert.match(result.stderr, /\[guardex-preedit-guard\] Codex edit\/commit detected on a protected branch\./);
assert.match(result.stderr, /bash scripts\/codex-agent\.sh/);
});

test('setup agent-branch-start requires --allow-in-place when using --in-place', () => {
const repoDir = initRepo();

Expand Down
Loading