rules/git-workflow.md
When pushing changes and creating PRs:
origin (the fork wwwillchen/dyad), then create a PR from the fork to the upstream repo (dyad-sh/dyad).upstream (dyad-sh/dyad) as a last resort.Bot account push permissions: The keppo-bot account does NOT have write access to upstream (dyad-sh/dyad). If a branch tracks upstream (e.g., upstream/claude/...), pushing will fail with a permission error. In this case, push to origin (the bot's fork at keppo-bot/dyad) instead:
git push --force-with-lease -u origin HEAD
This overrides the branch's tracking remote. Always check which remote origin points to (git remote -v) — for bot workspaces, origin is typically the bot's fork, not the upstream repo.
gh pr create branch detectionIf gh pr create says you must first push the current branch to a remote even though git push -u succeeded, create the PR with an explicit head ref:
gh pr create --head <owner>:<branch> ...
This can happen when remotes are configured in a non-fork layout and gh fails to infer the branch mapping.
If gh auth status succeeds but git push fails with Repo <owner>/<repo> is not allowlisted followed by fatal: could not read Username for 'https://github.com/...': Device not configured, run gh auth setup-git first and then push to an allowlisted remote. In some bot workspaces, fork remotes are not allowlisted even when upstream is, so retry the push against upstream if project policy permits it.
Before creating a PR for a freshly pushed branch, check whether it is actually ahead of the base branch:
git rev-list --left-right --count upstream/main...HEAD
If this returns 0 0, the branch has no commits ahead of upstream/main. GitHub cannot open a PR for an empty branch, so do not fabricate an empty commit just to satisfy gh pr create; report the branch as pushed but PR-blocked instead.
gh pr create fork-collab permission errorIf gh pr create from a fork fails with GraphQL: Fork collab Fork collab can't be granted by someone without permission (createPullRequest), add --no-maintainer-edit. gh defaults to enabling maintainer edits, which requires a permission the fork account does not have for the upstream repo.
gh pr create --repo dyad-sh/dyad --head <owner>:<branch> --no-maintainer-edit --title "..." --body "..."
gh pr create body quotingWhen passing a PR body inline via gh pr create --body "...", unescaped backticks are evaluated by zsh before gh runs. Avoid backticks in inline bodies, or use a body file / heredoc so literal code identifiers do not turn into command not found errors.
Add #skip-bugbot to the PR description for trivial PRs that won't affect end-users, such as:
When running GitHub Actions with pull_request_target on cross-repo PRs (from forks):
origin to the fork (head repo), not the base repoupstream remote: git remote add upstream https://github.com/<base-repo>.gitorigin → fork (push here), upstream → base repo (rebase from here)GITHUB_TOKEN can push to the fork if the PR author enabled "Allow edits from maintainers"claude-code-action overwrites origin's fetch URL to point to the base repo (using GITHUB_REPOSITORY). Any workflow that needs to push to the fork must set pushurl separately via git remote set-url --push origin <fork-url>, because git uses pushurl over url when both are configured. See pr-review-responder.yml and claude-rebase.yml for examples.Actions performed using the default GITHUB_TOKEN (including labels added by github-actions[bot] via actions/github-script) do not trigger pull_request_target or other workflow events. This is a GitHub limitation to prevent infinite loops. If one workflow adds a label that should trigger another workflow (e.g., label-rebase-prs.yml adds cc:rebase to trigger claude-rebase.yml), the label-adding step must use a PAT or GitHub App token (e.g., PR_RW_GITHUB_TOKEN) instead of GITHUB_TOKEN.
case allowlists in workflowsWhen matching GitHub bot logins in Bash case patterns, escape literal square brackets. For example, keppo-bot[bot] is parsed as a character class and does not match the login; use keppo-bot\[bot\].
When using gh api to post comments or replies containing backticks, $(), or other shell metacharacters, the security hook will block the command. Instead of passing the body inline with -f body="...", write a JSON file and use --input:
# Write JSON body to a file (use the Write tool, not echo/cat)
# File: .claude/tmp/reply_body.json
# {"body": "Your comment with `backticks` and special chars"}
gh api repos/dyad-sh/dyad/pulls/123/comments/456/replies --input .claude/tmp/reply_body.json
Similarly for GraphQL mutations, write the full query + variables as JSON and use --input:
# {"query": "mutation($threadId: ID!) { ... }", "variables": {"threadId": "PRRT_abc123"}}
gh api graphql --input .claude/tmp/resolve_thread.json
gh pr edit --add-label can fail for two reasons:
gh api repos/dyad-sh/dyad/issues/{PR_NUMBER}/labels -f "labels[]=label-name"
keppo-bot account (and similar bot/fork accounts) may not have permission to add labels on the upstream repo (dyad-sh/dyad). Both gh pr edit --add-label and the REST API will fail with 403/permission errors. In this case, skip label addition and note it in the PR summary rather than failing the workflow. Labels can be added later by a maintainer with appropriate permissions.In CI, claude-code-action restricts file access to the repo working directory (e.g., /home/runner/work/dyad/dyad). Skills that save intermediate files (like PR diffs) must use ./filename (current working directory), never /tmp/. Using /tmp/ causes errors like: cat in '/tmp/pr_*_diff.patch' was blocked. For security, Claude Code may only concatenate files from the allowed working directories.
When origin has separate fetch and push URLs (e.g., fetch → dyad-sh/dyad, push → keppo-bot/dyad), git push --force-with-lease fails with "stale info" after a rebase because the local tracking ref was refreshed from the fetch URL but does not reflect the push URL's state. In this specific split-remote configuration, use git push --force origin HEAD:
git push --force origin HEAD
Note: Plain --force can overwrite others' remote commits. Only use this in the split-remote scenario described above, where --force-with-lease cannot work. In normal setups, always prefer --force-with-lease.
In some Codex shells, pushing to fork remotes can fail immediately with Repo <owner>/<repo> is not allowlisted even when gh auth status shows a valid token. If both fork remotes are blocked this way but upstream is allowed, push the branch directly to upstream (for example git push --force-with-lease upstream HEAD:<branch>) and then repoint the local branch to track upstream/<branch> so later status and push commands reflect the real remote.
If git rebase fails with "You have unstaged changes" (common with spurious package-lock.json changes):
git stash push -m "Stashing changes before rebase"
git rebase upstream/main
git stash pop
The stashed changes will be automatically merged back after the rebase completes.
CONFLICT (modify/delete): <file> deleted in <commit> and modified in HEAD, use git rm <file> (not git add) to resolve by confirming the deletion. Use git add <file> only when you want to keep the modified version instead.GIT_EDITOR=true git rebase --continue in agent shells. Plain git rebase --continue can open vi for COMMIT_EDITMSG and fail with error: vi died of signal 15 when stdin is not interactive.npm install modified package-lock.json (common in CI/local), discard changes with git restore package-lock.json to avoid "unstaged changes" errors<<<<<<< HEAD with different imports), keep both imports if both are valid and needed by the component@/lib/schemas (e.g., DEFAULT_ZOOM_LEVEL)const iframe = po.previewPanel.getPreviewIframeElement()) that is referenced in non-conflicting code between or after conflict markers, keep the declaration even when adopting the other side's verification approach — the variable is needed regardless of which style you chooseflex items-start space-x-2 vs flex items-end gap-1), keep the newer styling from the incoming commit but preserve any functional components (like dialogs or modals) that exist in HEAD but not in the incoming changerunSingleStreamPass() and HEAD adds mid-turn compaction to the inline code, add compaction support to the new function rather than keeping the old inline versionIf you need to rebase but have uncommitted changes (e.g., package-lock.json from startup npm install):
git stash push -m "Stash changes before rebase"git rebase upstream/main (resolve conflicts if needed)git stash show -pgit stash dropgit stash pop and discard spurious changes: git restore package-lock.json (if package.json unchanged)This prevents rebase conflicts from uncommitted changes while preserving any work in progress.
When rebasing a PR branch that conflicts with upstream documentation changes (e.g., AGENTS.md):
rules/*.md files), keep upstream's versionWhen rebasing causes conflicts in the engines field of package.json (e.g., node version requirements), accept the incoming change from upstream/main to maintain consistency with the base branch requirements. The same resolution should be applied to the corresponding section in package-lock.json.