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
8 changes: 8 additions & 0 deletions claude-worktree.sh
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,14 @@ _cw_main() {
local suffix=$(LC_ALL=C tr -dc 'a-z0-9' </dev/urandom | head -c 8)
local branch_name="claude-${adj}-${name}-${suffix}"

# Defense-in-depth: reject if generated name matches a protected branch
case "$branch_name" in
main|master|develop)
echo "Error: Generated branch name '$branch_name' matches a protected branch."
return 1
;;
esac

# Check if we're in a git repository
if ! git rev-parse --is-inside-work-tree &>/dev/null; then
echo "Not in a git repo, launching claude normally..."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1148,46 +1148,57 @@ async function handler(input: StopInput): Promise<StopHookOutput> {

if (hasChanges) {
const branch = await getCurrentBranch(repoRoot);
const protectedBranches = ['main', 'master', 'develop'];
const isProtectedBranch = branch !== null && protectedBranches.includes(branch);

// Only stage non-gitignored files
const filesToStage = await getNonIgnoredChanges(repoRoot);
if (filesToStage.length === 0) {
// All changes are gitignored - skip commit
await logger.logOutput({ skipped: true, reason: 'All changes are gitignored' });
} else {
// Stage only non-ignored files
for (const file of filesToStage) {
await execCommand(`git add "${file}"`, repoRoot);
}
if (isProtectedBranch) {
await logger.logOutput({
skipped: true,
reason: `Refusing to auto-commit on protected branch: ${branch}`,
});
}

const commitMessage = formatCommitMessage(input.session_id, branch);
const commitResult = await execCommand(
`git commit -m ${JSON.stringify(commitMessage)}`,
repoRoot
);

if (commitResult.success) {
const shaResult = await execCommand('git rev-parse HEAD', repoRoot);
const fullSha = shaResult.success ? shaResult.stdout : null;
commitSha = fullSha ? fullSha.substring(0, 7) : 'unknown';
commitMade = true;

await logger.logOutput({ commit_made: true, commit_sha: commitSha });

// Update state with new commit SHA for tracking
await updateSessionStopState(input.session_id, {
blockCount: sessionState.blockCount + 1,
lastBlockTimestamp: new Date().toISOString(),
lastSeenCommitSha: fullSha || undefined,
}, repoRoot);

// Re-check branch sync after commit
const postCommitSync = await checkBranchSync(repoRoot);
syncCheck.aheadBy = postCommitSync.aheadBy;
// Also update commits ahead of main
commitsAheadOfMain = await getCommitsAheadOfMain(repoRoot);
if (!isProtectedBranch) {
// Only stage non-gitignored files
const filesToStage = await getNonIgnoredChanges(repoRoot);
if (filesToStage.length === 0) {
// All changes are gitignored - skip commit
await logger.logOutput({ skipped: true, reason: 'All changes are gitignored' });
} else {
await logger.logOutput({ commit_failed: true, error: commitResult.stderr });
// Stage only non-ignored files
for (const file of filesToStage) {
await execCommand(`git add "${file}"`, repoRoot);
}

const commitMessage = formatCommitMessage(input.session_id, branch);
const commitResult = await execCommand(
`git commit -m ${JSON.stringify(commitMessage)}`,
repoRoot
);

if (commitResult.success) {
const shaResult = await execCommand('git rev-parse HEAD', repoRoot);
const fullSha = shaResult.success ? shaResult.stdout : null;
commitSha = fullSha ? fullSha.substring(0, 7) : 'unknown';
commitMade = true;

await logger.logOutput({ commit_made: true, commit_sha: commitSha });

// Update state with new commit SHA for tracking
await updateSessionStopState(input.session_id, {
blockCount: sessionState.blockCount + 1,
lastBlockTimestamp: new Date().toISOString(),
lastSeenCommitSha: fullSha || undefined,
}, repoRoot);

// Re-check branch sync after commit
const postCommitSync = await checkBranchSync(repoRoot);
syncCheck.aheadBy = postCommitSync.aheadBy;
// Also update commits ahead of main
commitsAheadOfMain = await getCommitsAheadOfMain(repoRoot);
} else {
await logger.logOutput({ commit_failed: true, error: commitResult.stderr });
}
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions plugins/github-orchestration/hooks/commit-task-await-ci-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,18 @@ async function handler(
return {};
}

// Refuse to auto-commit on protected branches
const branchResult = await execCommand('git rev-parse --abbrev-ref HEAD', input.cwd);
const currentBranch = branchResult.success ? branchResult.stdout : null;
const protectedBranches = ['main', 'master', 'develop'];
if (currentBranch && protectedBranches.includes(currentBranch)) {
await logger.logOutput({
skipped: true,
reason: `Refusing to auto-commit on protected branch: ${currentBranch}`,
});
return {};
}

// Get task edits (file operations and prompt)
let taskEdits;
try {
Expand Down
Loading