Interactive CLI for organising your GitHub starred repositories into lists via GitHub's unofficial UI endpoints.
Project description
github-manage-stars-unofficial
Interactive CLI for organising your GitHub starred repositories into Stars Lists, using GitHub's unofficial UI endpoints.
$ ghstars
GitHub's "Stars Lists" feature is a UI-only thing — there is no public REST or GraphQL API for creating, deleting, or assigning repos to lists. This tool fills the gap by talking to the same endpoints your browser uses, so you can:
- Bulk-categorise hundreds (or thousands) of stars into themed lists
- Dump your full star library to JSON so you can feed it to an LLM and let the model pick categories
- Delete one, several, or all of your existing lists
- Resume mid-job after network hiccups (state is checkpointed after every action)
⚠️ Disclaimer — read before you use this
This project uses undocumented internal endpoints of github.com. There is no contract, no SLA, and no guarantee that any of it keeps working tomorrow.
Risks you accept by using this tool
- Endpoint breakage. GitHub can change the HTML structure, the CSRF flow, the URL paths, or the request shapes at any moment. When that happens, this tool stops working until someone updates it. There is no API version to pin against.
- Account flags / temporary throttling. You're driving the UI programmatically. Aggressive jitter settings (sub-second) or huge volumes can look like abuse. We default to 1.5–3.0s jitter between requests for a reason — keep it there or higher.
- Cookie exposure. This tool authenticates with your browser session cookies (
user_session,_gh_sess). Those are equivalent to your password. They live unencrypted in~/.config/ghstars/credentials.jsonwithchmod 600while the tool needs them, but if your filesystem is compromised, so is your GitHub account. Use theclear-credentialscommand (or the menu option) the moment you're done. - Hard 32-list limit. GitHub enforces a maximum of 32 lists per account. This tool refuses to attempt more.
- Stars Lists data is not in your normal backups. Deletion is permanent — there is no undo.
Project intent
The sole purpose of this project is to let users organise their own GitHub stars at a scale the official UI cannot reach. It is not a scraper, not a mass-action tool, and not designed to operate against accounts you do not own.
If GitHub asks us to take this project down, we will. We would much rather GitHub ship a real, supported API for Stars Lists — see PROPOSAL.md for the case we'd make. Until then, this tool exists as a stopgap for people who already use the feature heavily.
Install
This project uses uv for dependency management. Either install via uv or pip — both work because the project is a standard pyproject.toml package.
From PyPI (easiest)
pip install github-manage-stars-unofficial
# or
uv tool install github-manage-stars-unofficial
Either way you get the ghstars command on your PATH. The PyPI distribution name is long for discoverability; the CLI binary stays short.
From source
git clone https://github.com/snowfluke/github-manage-stars-unofficial.git
cd github-manage-stars-unofficial
uv sync
uv run ghstars
Requires Python 3.10+.
Quick start
ghstars # interactive menu (the easy mode)
ghstars setup # one-time: paste your session cookies
ghstars fetch # dump all your stars to JSON
ghstars apply --stars stars-you-202X.json # categorise + create + assign
The interactive menu walks you through every step with prompts and colour-coded output. Everything below is for users who prefer flags.
Recommended flow
┌─────────────────┐ ┌─────────────┐ ┌───────────┐ ┌─────────────┐ ┌──────────────┐
│ 1. setup │ -> │ 2. fetch │ -> │ 3. plan │ -> │ 4. apply │ -> │ 5. cleanup │
│ (paste cookies) │ │ (dump JSON) │ │ (preview) │ │ (mutations) │ │ (clear creds)│
└─────────────────┘ └─────────────┘ └───────────┘ └─────────────┘ └──────────────┘
- Setup — paste your
user_sessionand_gh_sesscookies once. The tool stores them under~/.config/ghstars/credentials.jsonwith mode600. - Fetch —
ghstars fetchwrites a clean JSON array of every starred repo (name, description, language, topics, etc.). This file is the right thing to hand to an LLM if you want AI-assisted categorisation. - Plan —
ghstars apply --phase plan --stars <file>shows you the proposed bucketing without touching GitHub. Iterate on your categories until happy. - Apply — same command without
--phase plancreates the lists and assigns repos. Progress is checkpointed; you can Ctrl-C and resume. - Cleanup —
ghstars clear-credentials, then rotate the session at https://github.com/settings/sessions.
Letting an LLM choose your categories
The categoriser is rule-based by default (32 built-in regex categories). For better fit to your library, dump your stars and ask a model to propose categories:
ghstars fetch --out stars.json
Open examples/ai-prompt-template.md for a prompt you can copy. Feed it stars.json and paste the resulting categories into a file matching examples/custom-categories.json. Then:
ghstars apply --stars stars.json --categories my-categories.json
The validator will refuse anything over 32 categories, any name over 32 chars, or any case-insensitive duplicate — those are GitHub's limits, not ours.
Recommended settings
The defaults err on the side of being slow and polite. Change them only if you understand the trade-off.
| Setting | Default | Notes |
|---|---|---|
jitter_min |
1.5s |
Minimum delay between mutating requests. Sub-second is not advised. |
jitter_max |
3.0s |
Upper bound; together with jitter_min you get random delays of 1.5–3.0s. |
phase_pause_min / phase_pause_max |
4.0–8.0s |
Pause between phases (delete-all → create-all → assign-all). |
retry_attempts |
4 |
Exponential-backoff retries on transient connection errors. |
retry_base_seconds |
2.0 |
Backoff base. Each retry waits base^attempt + random(0,1.5) seconds. |
A full run for ~500 stars takes 15–25 minutes with the defaults. Don't try to make this faster — the bottleneck is GitHub's tolerance, not your network.
Edit interactively with ghstars settings, or write ~/.config/ghstars/settings.json directly.
Commands
| Command | Description |
|---|---|
ghstars |
Open the interactive menu. |
ghstars setup |
Paste cookies once; stored in ~/.config/ghstars/credentials.json. |
ghstars status |
Show whether credentials are set, current settings, and resume state. |
| `ghstars fetch [--out FILE] [--format json | jsonl]` |
ghstars list-lists |
Print your current Stars Lists. |
ghstars delete-lists --all |
Delete every star list. Asks for confirmation. |
ghstars delete-lists --slugs a,b,c |
Delete specific lists by slug. |
ghstars apply --stars FILE [--categories FILE] [--wipe-existing] [--phase plan|create|assign|all] |
Full pipeline. |
ghstars settings |
Edit jitter / retry settings. |
ghstars clear-credentials |
Wipe stored cookies. |
Run ghstars <cmd> --help for full details.
File layout
Everything the tool persists lives under ~/.config/ghstars/:
~/.config/ghstars/
├── credentials.json # your cookies (chmod 600)
├── settings.json # jitter / retries
└── accounts/
└── <your-username>/
└── state.json # which lists are created, which repos assigned
Star dumps and your custom categories live wherever you run the tool from (typically your project / Downloads folder).
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
whoami fails on setup verification |
Wrong / expired cookies, or you copied them while logged out | Sign in again, re-copy cookies, redo setup |
Cannot have more than 32 lists |
You've already hit GitHub's account-wide cap | Delete some via ghstars delete-lists |
Name has already been taken |
Case-insensitive name collision | Rename your category — see validators.py |
Repeated RemoteDisconnected errors |
GitHub closing idle keep-alives, or transient network | The built-in retry handles it; increase retry_attempts if it persists |
| Want to start over | ghstars delete-lists --all then re-run apply |
How it works
For the curious, here's what's actually happening under the hood:
- Auth: session cookies (
user_session,_gh_sess) — same as your browser - CSRF: per-form
authenticity_tokenscraped from the relevant HTML page before each mutation - Create list:
POST /stars/{user}/lists(multipart form) - Delete list:
POST /stars/{user}/lists/{slug}with_method=delete(urlencoded) - List membership:
POST /{owner}/{repo}/listswith_method=putandlist_ids[](multipart) — note this is the full set of memberships, not a delta - Fetching stars: the documented REST endpoint
/users/{user}/starred(this part is official)
See src/ghstars/api.py if you want all the details — it's commented and ~250 lines.
Contributing
Bug reports and patches welcome. Reverse-engineered endpoints rot quickly — if something breaks, please open an issue with the failing request URL and the response body (sanitise cookies first).
uv sync --dev
uv run pytest
uv run ruff check .
The repo ships with 59 unit tests covering everything that doesn't require GitHub network access (parser, validators, categoriser, state, credentials). New rules or refactors should add corresponding tests.
Publishing (maintainers)
PyPI releases are automated by .github/workflows/release.yml — the workflow runs when you publish a GitHub Release.
Requirements (one-time):
- Create a PyPI API token at https://pypi.org/manage/account/token/.
- Add it as a repo secret named
PYPI_TOKEN(Settings → Secrets and variables → Actions). - (Optional) Set workflow permissions to "Read repository contents and packages permissions" — the workflow only uses
contents: read.
To cut a release:
- Bump the version in
pyproject.tomlandsrc/ghstars/__init__.py(they must match). - Update
CHANGELOG.md. - Commit, then tag:
git tag v0.x.y && git push --tags. - On GitHub: Releases → Draft a new release → Publish release (using that tag).
- The
releaseworkflow runs tests, builds the sdist + wheel, and publishes to PyPI.
If the tag's version doesn't match ghstars.__version__, the workflow fails before publishing.
Manual fallback
uv build # writes dist/ghstars-*.whl and -*.tar.gz
UV_PUBLISH_TOKEN=$PYPI_TOKEN uv publish
See also
PROPOSAL.md— what an official GitHub API for Stars Lists could look likeexamples/— sample stars dump, custom categories, AI prompt template
Licence
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file github_manage_stars_unofficial-0.1.1.tar.gz.
File metadata
- Download URL: github_manage_stars_unofficial-0.1.1.tar.gz
- Upload date:
- Size: 242.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
97d4bef62c5d996f2b98da242b128479d6eed348ba255a7458a6ef4c56b41779
|
|
| MD5 |
464cf24c63cc7699a9830f2d88d13695
|
|
| BLAKE2b-256 |
60d13113da12773dd5d4a058899e728134dbda62680e619fcc06a9d853ce8e91
|
File details
Details for the file github_manage_stars_unofficial-0.1.1-py3-none-any.whl.
File metadata
- Download URL: github_manage_stars_unofficial-0.1.1-py3-none-any.whl
- Upload date:
- Size: 33.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c967fa4acdb74b50d44d32ae1780a18f2bd7b639eaa380fde70a9652ed469563
|
|
| MD5 |
415aa1ebd168e131270ef822411bc95c
|
|
| BLAKE2b-256 |
0ad0e115105ace81f8e385fc62f5f62d89f3c55087c8449ef5e460e3a8f78a25
|