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.
When creating a new worktree branch from upstream/main with git worktree add -b <branch> <path> upstream/main, Git may set the new branch's upstream to upstream/main. Before using push helpers that push to the tracked remote, run git branch --unset-upstream or set the upstream to the actual feature branch to avoid treating main as the branch target.
If a PR's head branch is on another user's fork and gh pr view --json maintainerCanModify returns false, bot accounts cannot push fixes to that PR head even if review threads can be resolved. A fallback push to the base repo publishes the commit but does not update the original fork PR; call this out in the PR summary and ask the PR author or a maintainer to apply the published commit.
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.
npm run fmt may rewrite Markdown emphasis in .claude/skills/*.md. After
formatting, check git status and revert unrelated skill-file churn before
committing unless the task intentionally changes those skill docs.
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..claude/settings.json, which merges its permissions.allow list into the agent's effective allowlist. Strip it after checkout (or skip checkout) — see rules/claude-github-workflows.md for hardening guidance.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, 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 push, gh pr view, and gh auth status fail with only fetch failed, but unauthenticated git ls-remote https://github.com/dyad-sh/dyad HEAD works, the local gh-broker credential helper is unreachable rather than GitHub being down. Check the broker health/token path before retrying pushes; SSH is not a fallback unless ssh -T [email protected] succeeds.
If broker-backed commands fail with Unexpected token '<', "<!DOCTYPE "... is not valid JSON, the configured broker URL is returning an HTML error page instead of the token API response. Verify BROKER_BASE_URL and broker routes such as /healthz or /mint before changing remotes or retrying GitHub commands.
If git fetch --all fails on a contributor remote with would clobber existing tag, but the output shows Fetching upstream completed first, do not treat the rebase as blocked. Run git fetch upstream to confirm the base remote is current, then rebase onto upstream/main.
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 versione2e-tests/snapshots/*.txt, *.snap): When a rebase conflicts on a snapshot, neither side may match what the rebased code actually produces (e.g., upstream changed the system prompt, your branch added new tools). Resolve quickly with git checkout --theirs <file> to unblock the rebase, then regenerate snapshots after the rebase completes: npm test -- -u for vitest snapshots, and re-run the affected E2E spec with --update-snapshots for E2E .txt/.yml snapshots. The system-prompt snapshot in src/__tests__/__snapshots__/local_agent_prompt.test.ts.snap and the matching E2E snapshots often drift together — after rebasing, expect to update both.expect(contents).toContain("REQUIRED") for a phrase introduced in your AI rules patcher) will silently start asserting against text that was rebased away when upstream reworded the same section. After resolving the prose conflict, search for tests that reference the removed phrase (grep "REQUIRED" *.test.ts) and either delete the now-redundant assertion or update it to match the merged wording — the rebase itself does not surface this.src/ipc/utils/nitro_setup.ts) and an upstream commit later added a new step to the inline code (e.g., addNitroToViteConfig patching vite.config.ts from enable_nitro.ts), don't just take "ours" for the conflict. Port upstream's new step into your helper so the new feature still runs — otherwise the rebase silently drops upstream's feature for every caller of the helper.If 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.
When rebasing past an upstream release tag, package-lock.json may conflict only on the two top-level "version" fields (e.g., 0.45.0 vs your branch's older 0.45.0-beta.1). The lockfile's dependency tree is otherwise identical to upstream. Resolve by taking upstream's tree (git checkout --ours package-lock.json when rebasing onto upstream — ours is the rebase target during a git rebase), then manually edit the two "version" entries to match the current package.json version. Running npm install afterward is unnecessary just for this; only do it if a real dependency change requires regeneration.
npm install after taking either side of a package-lock.json conflictIf a package-lock.json conflict during rebase isn't a pure version-bump and you resolve it by taking one side wholesale (git checkout --ours package-lock.json or --theirs), run npm install before npm run ts / tests. Otherwise node_modules still reflects the pre-rebase lockfile, and tsc fails with Cannot find module '<pkg>' for any dependency that was added upstream during the rebase window. Symptom: typecheck errors on packages you never touched in your branch.