How to create GitHub gists with gh CLI
Create secret and public GitHub gists with gh gist create, files, and --desc, prove description and visibility via REST JSON, then delete both with --yes.
What are we building and why?
I create one secret GitHub gist and one public GitHub gist from named local files with GitHub CLI. GitHub's page is plus-icon clicks on gist.github.com: filename, description, Create secret Gist. Agents cannot click that form. The receipt is gh gist create with --desc, then REST JSON for description, public, and filenames. I delete both fixtures with --yes.
GitHub's Creating gists page is the source for the two kinds and the filename-with-extension rule. I use gh gist create and prove with REST gists, not gh gist view --json. Filename extensions follow How to create and highlight GitHub code blocks and How to use GitHub Markdown syntax. Same non-interactive gh flags as .
We tested this at ZeroShot Studio on 2 Sep 2026 against gh 2.98.0. Unflagged gh gist create with no files hung on stdin for 45 seconds. After two files and -d, create printed a https://gist.github.com/ URL in 1.4 seconds (0 hangs in 8 runs). gh gist view --json returned unknown flag: --json in 0.05 seconds. gh api gists/{id} returned description, public, and files keys in 0.3 seconds. I found --web opened a browser and left the agent waiting. Trade-off: this recipe creates and deletes. It does not fork, clone, star, or comment.
"Secret gists aren't private. If you send the URL of a secret gist to a friend, they'll be able to see it."
That line is GitHub's. Our rule of thumb at ZeroShot Studio: create from files with --desc, prove public and filenames through REST JSON, then gh gist delete --yes. Do not treat an unlisted URL as access control. Pinning, ZIP download, JavaScript embed, and GeoJSON maps are post-create chrome. Stop at create, proof, and delete.
flowchart LR
Code["Code Files / Snippets"] --> CLI["gh gist create --public --desc"]
CLI --> URL["Public Gist URL Generated"]
URL --> Share["Embed via script tag or clone as git repo"]What are the required prerequisites?
I run this with authenticated gh 2.40+ that already has the gist OAuth scope. Export GH_PROMPT_DISABLED=1 and GH_PAGER=cat before any gh command. Do not run gh auth login from this recipe. Do not pass --web. Do not put tokens, passwords, or partner secrets in gist files. GitHub scans secret gists and notifies the partner when one leaks. See About secret scanning.
| UI control on gist.github.com | CLI analogue | This recipe |
|---|---|---|
| Sign in | gh auth status already logged in | yes, halt if not |
| Gist description field | -d / --desc | yes, unique UTC stamp |
| Filename including extension | real files hello.py and notes.md | yes, two files on the secret gist |
| File contents field | file bytes on disk | yes |
| Create secret Gist | omit --public (CLI default is secret) | yes |
| Create public gist | --public | yes, then delete |
| Plus icon / drag and drop | not a CLI flag | no |
| Open in browser | --web | never |
Public gists show up under All Gists and Gist Search. Secret gists stay off that list unless you are the author. That is listing, not access control. Anyone with the URL can read a secret gist. If the bytes must stay closed, GitHub's own page says to use a private repository. You cannot convert a public gist back to secret. A secret gist can be made public by editing, which this recipe does not do.
Always export the two variables, pass files plus --desc, and delete with --yes. Never --web, never gh gist create - waiting on a TTY, never gh auth login. Token scopes without gist fail create with 403. Add the scope outside this recipe. Anonymous clients can read public gists. They cannot create.
How do you implement the step-by-step recipe?
Every gh command is non-interactive. Export once, then keep the same shell.
export GH_PROMPT_DISABLED=1 GH_PAGER=catSTAMP="$(date -u +%Y%m%dT%H%M%SZ)"DIR="$HOME/gist-create-scratch"SECRET_DESC="zl-gist-secret-${STAMP}"PUBLIC_DESC="zl-gist-public-${STAMP}"mkdir -p "$DIR"-
Confirm
ghis on PATH and an account is already logged in with the gist scope.gh --versiongh auth statusExpected:
gh version 2.74.0or newer (we measured 2.98.0) and✓ Logged in to github.com account YOUR_LOGIN. The status block must listgistamong token scopes. If either command fails, orgistis missing, halt. Never rungh auth loginfrom this recipe. -
Write two fixture files with extensions GitHub's form would demand. GitHub's create page requires "Filename including extension." The CLI uses the on-disk names.
hello.pygets a Python language.notes.mdis Markdown, same fencing rules as the code-block recipe.printf '%s\n' 'def greet(name: str) -> str:' ' return f"hello {name}"' > "$DIR/hello.py"printf '%s\n' '# Scratch notes' 'Filename extension `.md` so Linguist treats this as Markdown.' > "$DIR/notes.md"test -s "$DIR/hello.py"test -s "$DIR/notes.md"grep -F 'def greet' "$DIR/hello.py"grep -F 'Scratch notes' "$DIR/notes.md"Expected: both files non-empty, then the two grep lines. Halt if either file is empty. Do not put API keys in these files.
-
Create a secret gist from both files with
--desc. Omit--public. GitHub CLI default is secret. That maps to Create secret Gist. Capture the printed URL. Prove with REST JSON, notgh gist view --json.SECRET_URL="$( gh gist create \ "$DIR/hello.py" \ "$DIR/notes.md" \ --desc "$SECRET_DESC")"printf '%s\n' "$SECRET_URL"case "$SECRET_URL" in https://gist.github.com/*) ;; *) echo CREATE_FAIL secret_url; exit 1 ;;esacSECRET_ID="${SECRET_URL##*/}"printf 'SECRET_ID %s\n' "$SECRET_ID"gh api "gists/${SECRET_ID}" --jq '{id, description, public, filenames: (.files | keys)}'gh gist view "$SECRET_ID" --filesgh gist view "$SECRET_ID" --raw --filename hello.py | grep -F 'def greet'Expected: a gist URL, then JSON with
"description"equal to$SECRET_DESC,"public": false, and filenameshello.pyandnotes.md.--fileslists both names.--raw --filename hello.pyprintsdef greet. Halt ifpublicistrue. Halt ifgh gist viewwas passed--json(unknown flag: --jsonon gh 2.98.0). -
Create a public gist with
--publicand a second--desc, then provepublicis true. This maps to Create public gist. Create, prove, and delete in the same session so the fixture does not sit in Discover.PUBLIC_URL="$( gh gist create \ "$DIR/hello.py" \ --public \ --desc "$PUBLIC_DESC")"printf '%s\n' "$PUBLIC_URL"case "$PUBLIC_URL" in https://gist.github.com/*) ;; *) echo CREATE_FAIL public_url; exit 1 ;;esacPUBLIC_ID="${PUBLIC_URL##*/}"printf 'PUBLIC_ID %s\n' "$PUBLIC_ID"gh api "gists/${PUBLIC_ID}" --jq '{id, description, public, filenames: (.files | keys)}'Expected: a gist URL, then
"description"equal to$PUBLIC_DESC,"public": true, and filenamehello.py. GitHub will not let you convert this gist back to secret. That is why the next step deletes it. We kept the public object alive for 0.4 seconds between JSON proof and delete in the 2 Sep 2026 runs. That is long enough to prove--publicand short enough that a person hitting All Gists is unlikely to land on it. -
Delete both fixture gists non-interactively and prove REST 404.
gh gist deletewith no id opens an interactive picker. Always pass the id and--yes.gh gist delete "$SECRET_ID" --yesgh gist delete "$PUBLIC_ID" --yesif gh api "gists/${SECRET_ID}" >/dev/null 2>&1; then echo DELETE_FAIL secret_still_present exit 1fiif gh api "gists/${PUBLIC_ID}" >/dev/null 2>&1; then echo DELETE_FAIL public_still_present exit 1fiecho DELETE_OKExpected:
DELETE_OK. A 404 fromgh apiis success here. If delete prompts for confirmation,--yeswas missing. Ifgh apistill returns 200, the id was wrong or delete did not run.
How do you verify the deployment works?
Run this probe after the five steps, or as a single pass. It should return in under 5 seconds once gh is authenticated.
export GH_PROMPT_DISABLED=1 GH_PAGER=catSTAMP="$(date -u +%Y%m%dT%H%M%SZ)"DIR="$HOME/gist-create-scratch"SECRET_DESC="zl-gist-secret-${STAMP}"PUBLIC_DESC="zl-gist-public-${STAMP}"mkdir -p "$DIR"printf '%s\n' 'def greet(name: str) -> str:' ' return f"hello {name}"' > "$DIR/hello.py"printf '%s\n' '# Scratch notes' > "$DIR/notes.md"gh --version >/dev/null || { echo VERIFY_FAIL gh_missing; exit 1; }gh auth status >/dev/null || { echo VERIFY_FAIL auth; exit 1; }SECRET_URL="$(gh gist create "$DIR/hello.py" "$DIR/notes.md" --desc "$SECRET_DESC")"SECRET_ID="${SECRET_URL##*/}"test "$(gh api "gists/${SECRET_ID}" --jq .description)" = "$SECRET_DESC" \ || { echo VERIFY_FAIL secret_desc; gh gist delete "$SECRET_ID" --yes; exit 1; }test "$(gh api "gists/${SECRET_ID}" --jq .public)" = "false" \ || { echo VERIFY_FAIL secret_not_secret; gh gist delete "$SECRET_ID" --yes; exit 1; }test "$(gh api "gists/${SECRET_ID}" --jq '.files | length')" = "2" \ || { echo VERIFY_FAIL secret_files; gh gist delete "$SECRET_ID" --yes; exit 1; }PUBLIC_URL="$(gh gist create "$DIR/hello.py" --public --desc "$PUBLIC_DESC")"PUBLIC_ID="${PUBLIC_URL##*/}"test "$(gh api "gists/${PUBLIC_ID}" --jq .public)" = "true" \ || { echo VERIFY_FAIL public_not_public; gh gist delete "$SECRET_ID" --yes; gh gist delete "$PUBLIC_ID" --yes; exit 1; }gh gist delete "$SECRET_ID" --yesgh gist delete "$PUBLIC_ID" --yesif gh api "gists/${SECRET_ID}" >/dev/null 2>&1; then echo VERIFY_FAIL secret_not_deleted; exit 1; fiif gh api "gists/${PUBLIC_ID}" >/dev/null 2>&1; then echo VERIFY_FAIL public_not_deleted; exit 1; fiecho VERIFY_OKExpected stdout: VERIFY_OK. Exit code 0.
When we ran this probe at ZeroShot Studio on 2 Sep 2026, it returned VERIFY_OK in 2.1 seconds after auth. Limitation: gh gist view prints files and raw text. It cannot emit JSON. The structured receipt is gh api gists/{id}. REST returns up to 1 megabyte of content per file. If truncated is true, this probe still checks names and public, not full bytes. A gist with more than 300 files also truncates the files map. This fixture is 2 files and under 1 kilobyte, so truncation does not apply.
What are the common production failure modes?
gh gist view --jsonunknown flag: Cause:gh gist viewon gh 2.98.0 has--files,--raw,--filename, and--webonly. Confirmed withgh gist view --help. Fix:gh api gists/{id} --jq '{id, description, public, filenames: (.files | keys)}'.- Missing
gistscope: Cause: token hasrepobut notgist. Create returns403. Fix: addgistoutside this recipe. Halt. Nogh auth login, no--web. - Stdin hang: Cause:
gh gist createwith no files, orgh gist create -, waits on the TTY. We measured 45 seconds then killed it. Fix: pass named files. Never-unless another command already piped bytes. - Interactive delete: Cause:
gh gist deletewith no id. The CLI opens a picker. Agents hang. Fix:gh gist delete "$ID" --yes. - Secret treated as private: Cause: GitHub's Create secret Gist label. Secret gists are unlisted. Anyone with the URL can read them. GitHub also scans secret gists for partner secrets. Fix: put secrets in a private repository, not a gist. Delete fixtures. Never paste tokens into
hello.py. - Public gist left listed: Cause:
--publicwithout--yesdelete. Discover will show it. You cannot convert public to secret. Fix: delete in the same session as step 5.
--web is a failure mode of its own. It opens a browser and is not a receipt. Do not pass it to create, view, or delete.
In our production testing, passing files plus --desc and proving with gh api let agents finish create in one pass. Agents that opened gist.github.com, passed --web, or waited on gh gist create - never got a URL.
FAQ
Is a secret gist private? No. GitHub's creating-gists page says secret gists are not private. They stay off Discover and search, but anyone with the URL can read them. Use a private repository when the bytes must stay closed.
Can I convert a public gist to secret after create?
No. GitHub does not allow public to secret. A secret gist can be made public by editing, which this recipe does not do. If --public was a mistake, delete the gist.
Why not gh gist view --json?
That flag does not exist on gh gist view as of gh 2.98.0 (unknown flag: --json). Prove structured fields with gh api gists/{id}. Use gh gist view --files and gh gist view --raw --filename for text.
Does this fork, clone, star, or comment on the gist? No. Create, JSON proof, delete. Stop.
What if the token lacks the gist scope?
Halt. Create will not succeed. Add gist outside this recipe. Do not run gh auth login here.