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
18 changes: 17 additions & 1 deletion .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n
is_vscode_git_context=1
fi

allow_vscode_protected_raw="${MUSAFETY_ALLOW_VSCODE_PROTECTED_BRANCH_WRITES:-$(git config --get multiagent.allowVscodeProtectedBranchWrites || true)}"
if [[ -z "$allow_vscode_protected_raw" ]]; then
allow_vscode_protected_raw="false"
fi
allow_vscode_protected="$(printf '%s' "$allow_vscode_protected_raw" | tr '[:upper:]' '[:lower:]')"

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

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 Down Expand Up @@ -111,7 +124,7 @@ MSG
fi

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

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

Optional repo override for manual VS Code protected-branch commits:
git config multiagent.allowVscodeProtectedBranchWrites true

Temporary bypass (not recommended):
ALLOW_COMMIT_ON_PROTECTED_BRANCH=1 git commit ...
MSG
Expand Down
21 changes: 18 additions & 3 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n
is_vscode_git_context=1
fi

allow_vscode_protected_raw="${MUSAFETY_ALLOW_VSCODE_PROTECTED_BRANCH_WRITES:-$(git config --get multiagent.allowVscodeProtectedBranchWrites || true)}"
if [[ -z "$allow_vscode_protected_raw" ]]; then
allow_vscode_protected_raw="false"
fi
allow_vscode_protected="$(printf '%s' "$allow_vscode_protected_raw" | tr '[:upper:]' '[:lower:]')"

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

is_codex_session=0
if [[ -n "${CODEX_THREAD_ID:-}" || -n "${OMX_SESSION_ID:-}" || "${CODEX_CI:-0}" == "1" ]]; then
is_codex_session=1
Expand Down Expand Up @@ -56,14 +69,16 @@ if [[ "${#blocked_refs[@]}" -gt 0 ]]; then
exit 1
fi

if [[ "$is_vscode_git_context" == "1" ]]; then
if [[ "$is_vscode_git_context" == "1" && "$allow_vscode_protected_branch_writes" == "1" ]]; then
exit 0
fi

{
echo "[agent-branch-guard] Push to protected branch blocked outside VS Code Git context."
echo "[agent-branch-guard] Push to protected branch blocked."
echo "[agent-branch-guard] Protected target(s): ${blocked_refs[*]}"
echo "[agent-branch-guard] Use VS Code Source Control for protected-branch push, or push from an agent branch and merge via PR."
echo "[agent-branch-guard] Use an agent branch and merge via PR."
echo "[agent-branch-guard] Optional VS Code override:"
echo " git config multiagent.allowVscodeProtectedBranchWrites true"
echo
echo "Temporary bypass (not recommended):"
echo " ALLOW_PUSH_ON_PROTECTED_BRANCH=1 git push ..."
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ Note: the monitor dispatches Codex through explicit `--task/--agent/--base` flag
- `gx setup` checks GitHub CLI (`gh`) and prints install guidance if missing.
- Interactive self-update prompt defaults to **No** (`[y/N]`).
- In initialized repos, `setup`/`install`/`fix` block protected-base writes unless explicitly overridden.
- In VS Code Source Control, manual (non-Codex) commits/pushes to protected branches are allowed by default.
- Direct commits/pushes to protected branches are blocked by default (including VS Code Source Control).
- Optional repo override for manual VS Code protected-branch writes: `git config multiagent.allowVscodeProtectedBranchWrites true`.
- Codex/agent sessions stay blocked on protected branches and must use `agent/*` branch + PR workflow.
- On protected `main`, `gx doctor` auto-runs in a sandbox agent branch/worktree.
- `scripts/agent-branch-start.sh` hydrates `scripts/codex-agent.sh` into new sandbox worktrees when missing, so auto-finish launcher flow stays available.
Expand Down
18 changes: 17 additions & 1 deletion templates/githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n
is_vscode_git_context=1
fi

allow_vscode_protected_raw="${MUSAFETY_ALLOW_VSCODE_PROTECTED_BRANCH_WRITES:-$(git config --get multiagent.allowVscodeProtectedBranchWrites || true)}"
if [[ -z "$allow_vscode_protected_raw" ]]; then
allow_vscode_protected_raw="false"
fi
allow_vscode_protected="$(printf '%s' "$allow_vscode_protected_raw" | tr '[:upper:]' '[:lower:]')"

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

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 Down Expand Up @@ -111,7 +124,7 @@ MSG
fi

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

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

Optional repo override for manual VS Code protected-branch commits:
git config multiagent.allowVscodeProtectedBranchWrites true

Temporary bypass (not recommended):
ALLOW_COMMIT_ON_PROTECTED_BRANCH=1 git commit ...
MSG
Expand Down
21 changes: 18 additions & 3 deletions templates/githooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n
is_vscode_git_context=1
fi

allow_vscode_protected_raw="${MUSAFETY_ALLOW_VSCODE_PROTECTED_BRANCH_WRITES:-$(git config --get multiagent.allowVscodeProtectedBranchWrites || true)}"
if [[ -z "$allow_vscode_protected_raw" ]]; then
allow_vscode_protected_raw="false"
fi
allow_vscode_protected="$(printf '%s' "$allow_vscode_protected_raw" | tr '[:upper:]' '[:lower:]')"

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

is_codex_session=0
if [[ -n "${CODEX_THREAD_ID:-}" || -n "${OMX_SESSION_ID:-}" || "${CODEX_CI:-0}" == "1" ]]; then
is_codex_session=1
Expand Down Expand Up @@ -56,14 +69,16 @@ if [[ "${#blocked_refs[@]}" -gt 0 ]]; then
exit 1
fi

if [[ "$is_vscode_git_context" == "1" ]]; then
if [[ "$is_vscode_git_context" == "1" && "$allow_vscode_protected_branch_writes" == "1" ]]; then
exit 0
fi

{
echo "[agent-branch-guard] Push to protected branch blocked outside VS Code Git context."
echo "[agent-branch-guard] Push to protected branch blocked."
echo "[agent-branch-guard] Protected target(s): ${blocked_refs[*]}"
echo "[agent-branch-guard] Use VS Code Source Control for protected-branch push, or push from an agent branch and merge via PR."
echo "[agent-branch-guard] Use an agent branch and merge via PR."
echo "[agent-branch-guard] Optional VS Code override:"
echo " git config multiagent.allowVscodeProtectedBranchWrites true"
echo
echo "Temporary bypass (not recommended):"
echo " ALLOW_PUSH_ON_PROTECTED_BRANCH=1 git push ..."
Expand Down
69 changes: 65 additions & 4 deletions test/install.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1054,7 +1054,7 @@ test('protect command manages configured protected branches', () => {
assert.match(result.stdout, /reset to defaults/);
});

test('pre-commit allows non-codex VS Code commits on custom protected branches configured via musafety protect', () => {
test('pre-commit blocks non-codex VS Code commits on custom protected branches by default', () => {
const repoDir = initRepoOnBranch('release');
seedCommit(repoDir);

Expand All @@ -1068,16 +1068,70 @@ test('pre-commit allows non-codex VS Code commits on custom protected branches c
ALLOW_COMMIT_ON_PROTECTED_BRANCH: '0',
VSCODE_GIT_IPC_HANDLE: '1',
});
assert.equal(hookResult.status, 0, hookResult.stderr || hookResult.stdout);
assert.equal(hookResult.status, 1, hookResult.stderr || hookResult.stdout);
assert.match(hookResult.stderr, /\[agent-branch-guard\] Direct commits on protected branches are blocked\./);
});

test('pre-commit blocks non-codex protected branch commits from VS Code Source Control env by default', () => {
const repoDir = initRepo();
seedCommit(repoDir);

const setupResult = runNode(['setup', '--target', repoDir, '--no-global-install'], repoDir);
assert.equal(setupResult.status, 0, setupResult.stderr || setupResult.stdout);

const hookResult = runCmd(
'bash',
['.githooks/pre-commit'],
repoDir,
{
ALLOW_COMMIT_ON_PROTECTED_BRANCH: '0',
VSCODE_GIT_IPC_HANDLE: '1',
VSCODE_GIT_ASKPASS_NODE: '1',
VSCODE_IPC_HOOK_CLI: '1',
},
);
assert.equal(hookResult.status, 1, hookResult.stderr || hookResult.stdout);
assert.match(hookResult.stderr, /\[agent-branch-guard\] Direct commits on protected branches are blocked\./);
});

test('pre-push blocks non-codex protected branch pushes from VS Code Source Control env by default', () => {
const repoDir = initRepoOnBranch('main');
seedCommit(repoDir);

const setupResult = runNode(['setup', '--target', repoDir, '--no-global-install'], repoDir);
assert.equal(setupResult.status, 0, setupResult.stderr || setupResult.stdout);

const hookResult = runCmd(
'bash',
[
'-lc',
`printf '%s\\n' 'refs/heads/main 1111111111111111111111111111111111111111 refs/heads/main 0000000000000000000000000000000000000000' | .githooks/pre-push origin origin`,
],
repoDir,
{
VSCODE_GIT_IPC_HANDLE: '1',
VSCODE_GIT_ASKPASS_NODE: '1',
VSCODE_IPC_HOOK_CLI: '1',
},
);
assert.equal(hookResult.status, 1, hookResult.stderr || hookResult.stdout);
assert.match(hookResult.stderr, /\[agent-branch-guard\] Push to protected branch blocked\./);
});

test('pre-commit allows non-codex protected branch commits from VS Code Source Control env', () => {
test('pre-commit allows non-codex protected branch commits from VS Code Source Control env when explicitly enabled', () => {
const repoDir = initRepo();
seedCommit(repoDir);

const setupResult = runNode(['setup', '--target', repoDir, '--no-global-install'], repoDir);
assert.equal(setupResult.status, 0, setupResult.stderr || setupResult.stdout);

let configResult = runCmd(
'git',
['config', 'multiagent.allowVscodeProtectedBranchWrites', 'true'],
repoDir,
);
assert.equal(configResult.status, 0, configResult.stderr || configResult.stdout);

const hookResult = runCmd(
'bash',
['.githooks/pre-commit'],
Expand All @@ -1092,13 +1146,20 @@ test('pre-commit allows non-codex protected branch commits from VS Code Source C
assert.equal(hookResult.status, 0, hookResult.stderr || hookResult.stdout);
});

test('pre-push allows non-codex protected branch pushes from VS Code Source Control env', () => {
test('pre-push allows non-codex protected branch pushes from VS Code Source Control env when explicitly enabled', () => {
const repoDir = initRepoOnBranch('main');
seedCommit(repoDir);

const setupResult = runNode(['setup', '--target', repoDir, '--no-global-install'], repoDir);
assert.equal(setupResult.status, 0, setupResult.stderr || setupResult.stdout);

let configResult = runCmd(
'git',
['config', 'multiagent.allowVscodeProtectedBranchWrites', 'true'],
repoDir,
);
assert.equal(configResult.status, 0, configResult.stderr || configResult.stdout);

const hookResult = runCmd(
'bash',
[
Expand Down
Loading