| name | oss-fork-manager |
| description | Fork lifecycle management for open-source contributions — fork creation, clone, upstream sync, feature branches, PR creation targeting upstream |
| keywords | fork, clone, upstream, sync, rebase, feature branch, open source, oss, remote, push, PR, pull request, contribution |
| related | ["oss-repo-recon","oss-issue-scout","pr-lifecycle"] |
OSS Fork Manager
Handle the fork-based contribution workflow that external open-source repos require.
When to Use
- Setting up a fork for the first time
- Syncing fork with upstream before starting work
- Creating feature branches for contributions
- Pushing to fork and creating PRs targeting upstream
Non-Negotiable Rules
| # | Rule | Violation Example | Correct Pattern |
|---|
| 1 | Check if fork exists before creating | gh repo fork failing because fork exists | Check first, then create only if needed |
| 2 | Always set upstream remote | Pushing to wrong remote | git remote add upstream https://github.com/{owner}/{repo}.git |
| 3 | Rebase on upstream before branching | Branching from stale main | git fetch upstream && git rebase upstream/{default} |
| 4 | Push to origin (fork), PR to upstream | git push upstream | git push origin {branch} then gh pr create -R {upstream} |
| 5 | Track state in fork-state.json | Losing track of branches and PRs | Update ~/.claude/{repoSlug}-recon/fork-state.json |
| 6 | Initialize contributions.json for new repos | Re-investigating already-attempted issues | Create from template ~/.claude/oss-contribution-tracking-template.json if missing |
Parameters
- repoFullName:
owner/repo (upstream)
- repoSlug: URL-safe slug for state directory
- forkName:
<your-login>/repo (your fork; login via gh api user --jq .login)
- localPath:
$HOME/git/repo (local clone)
- defaultBranch:
main (or master, develop)
Fork State Schema
File: ~/.claude/{repoSlug}-recon/fork-state.json
{
"version": 1,
"upstream": "owner/repo",
"fork": "<your-login>/repo",
"local_path": "$HOME/git/repo",
"default_branch": "main",
"last_upstream_sync": "2026-03-28T00:00:00Z",
"active_branches": []
}
Note: PR tracking and issue selection are in contributions.json (see Contribution Tracking below), not fork-state.json.
Contribution Tracking Schema
File: ~/.claude/{repoSlug}-recon/contributions.json
Template: ~/.claude/oss-contribution-tracking-template.json
Tracks all issues attempted and PRs created. Enables dedup (don't re-investigate issues) and rate limiting (max PRs per day). Initialized from the template during fork setup.
{
"schema_version": 1,
"repo": "owner/repo",
"config": {
"max_prs_per_day": 2,
"cooldown_after_close_hours": 48
},
"issues": {},
"prs": {},
"daily_log": {}
}
Issue statuses: selected → in-progress → submitted | skipped | abandoned
PR statuses: open → merged | closed
Operations
Create Fork (idempotent)
UPSTREAM="{{repoFullName}}"
FORK="{{forkName}}"
if gh api "repos/${FORK}" --jq '.full_name' 2>/dev/null; then
echo "Fork ${FORK} already exists"
else
gh repo fork "${UPSTREAM}" --clone=false
echo "Fork created"
fi
Clone Locally (idempotent)
LOCAL="{{localPath}}"
if [ -d "${LOCAL}/.git" ]; then
echo "Clone already exists at ${LOCAL}"
else
gh repo clone "${FORK}" "${LOCAL}"
cd "${LOCAL}"
git remote add upstream "https://github.com/${UPSTREAM}.git"
echo "Cloned and upstream remote added"
fi
Sync With Upstream
cd "{{localPath}}"
DEFAULT="{{defaultBranch}}"
git fetch upstream
git checkout "${DEFAULT}"
git rebase "upstream/${DEFAULT}"
git push origin "${DEFAULT}"
SLUG="{{repoSlug}}"
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
cat ~/.claude/${SLUG}-recon/fork-state.json | jq \
".last_upstream_sync = \"${NOW}\"" \
> /tmp/fork-state-tmp.json && mv /tmp/fork-state-tmp.json ~/.claude/${SLUG}-recon/fork-state.json
Create Feature Branch
cd "{{localPath}}"
UPSTREAM="{{repoFullName}}"
DEFAULT="{{defaultBranch}}"
BRANCH_NAME="{type}/{issue-number}-{slug}"
git fetch upstream
git checkout -b "${BRANCH_NAME}" "upstream/${DEFAULT}"
Branch naming conventions:
fix/{issue}-{slug} for bug fixes
feat/{issue}-{slug} for features
perf/{issue}-{slug} for performance improvements
docs/{issue}-{slug} for documentation
test/{issue}-{slug} for test additions
Push to Fork
cd "{{localPath}}"
git push origin "${BRANCH_NAME}"
Create PR Targeting Upstream
UPSTREAM="{{repoFullName}}"
FORK_OWNER="$(gh api user --jq .login)"
BRANCH_NAME="{branch}"
DEFAULT="{{defaultBranch}}"
gh pr create -R "${UPSTREAM}" \
--head "${FORK_OWNER}:${BRANCH_NAME}" \
--base "${DEFAULT}" \
--title "{title}" \
--body "$(cat <<'EOF'
{PR body following repo's template from recon report}
EOF
)"
Record PR in Contributions
BASE is the branch this PR targets upstream — the exact value passed to
gh pr create --base. Record it as original_base so future rebases (e.g.
after an upstream base-branch policy change) can use the correct 3-ref
git rebase --onto <new_base> <original_base> <branch> form without
guessing from the current PR state.
SLUG="{{repoSlug}}"
PR_NUM={pr_number}
BRANCH="{branch}"
BASE="{base}"
ISSUE_NUM={issue_number}
PR_TITLE="{title}"
PR_URL="{url}"
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
TODAY=$(date -u +%Y-%m-%d)
cat ~/.claude/${SLUG}-recon/contributions.json | jq \
--arg inum "${ISSUE_NUM}" \
--arg pnum "${PR_NUM}" \
--arg now "${NOW}" \
--arg today "${TODAY}" \
--arg branch "${BRANCH}" \
--arg base "${BASE}" \
--arg title "${PR_TITLE}" \
--arg url "${PR_URL}" \
'.issues[$inum].status = "submitted" |
.issues[$inum].pr_number = ($pnum | tonumber) |
.issues[$inum].updated_at = $now |
.prs[$pnum] = {
"number": ($pnum | tonumber),
"issue_number": ($inum | tonumber),
"branch": $branch,
"original_base": $base,
"title": $title,
"status": "open",
"created_at": $now,
"updated_at": $now,
"merged_at": null,
"closed_at": null,
"url": $url
} |
.daily_log[$today].prs_created += [($pnum | tonumber)]' \
> /tmp/contrib-tmp.json && mv /tmp/contrib-tmp.json ~/.claude/${SLUG}-recon/contributions.json
Initialization
Create fork-state.json and contributions.json if they don't exist:
SLUG="{{repoSlug}}"
REPO="{{repoFullName}}"
mkdir -p ~/.claude/${SLUG}-recon
if [ ! -f ~/.claude/${SLUG}-recon/fork-state.json ]; then
cat > ~/.claude/${SLUG}-recon/fork-state.json << EOF
{
"version": 1,
"upstream": "${REPO}",
"fork": "{{forkName}}",
"local_path": "{{localPath}}",
"default_branch": "{{defaultBranch}}",
"last_upstream_sync": null,
"active_branches": []
}
EOF
fi
if [ ! -f ~/.claude/${SLUG}-recon/contributions.json ]; then
sed "s|OWNER/REPO|${REPO}|" ~/.claude/oss-contribution-tracking-template.json \
> ~/.claude/${SLUG}-recon/contributions.json
fi
Anti-Patterns
See Also
- [[oss-repo-recon]] — Run before fork setup to know the default branch and conventions
- [[oss-issue-scout]] — Find issues before creating feature branches
- [[pr-lifecycle]] — PR management after creation