Intermediate
This tutorial covers intermediate GitHub CLI concepts through 28 self-contained, heavily annotated examples. The examples build on beginner authentication and repository skills to cover pull request review workflows, release management, gist operations, GitHub Actions run management, secrets, variables, labels, SSH keys, and Codespaces — spanning 35–70% of GitHub CLI features.
Advanced Pull Request Workflows
Example 29: Review a Pull Request
gh pr review submits an approval, request-for-changes, or comment review on a pull request,
with the option to compose a review body inline or in an editor.
# Approve PR #45 with a comment.
gh pr review 45 --approve --body "LGTM! The CSS fix looks correct and tests pass."
# => ✓ Approved pull request #45
# Request changes with detailed feedback.
gh pr review 45 --request-changes \
--body "Please add unit tests for the media query breakpoints before merging."
# => ✓ Requested changes on pull request #45
# Submit a general comment review without approving or blocking.
gh pr review 45 --comment --body "Left some minor nit suggestions inline."
# => ✓ Reviewed pull request #45
# Open an editor to write the review body interactively.
gh pr review 45 --approve
# => (editor opens for review body; submitted when file is saved and closed)Key takeaway: gh pr review --approve is the fastest way to approve a PR from the terminal
while staying in the code review context without navigating to the browser.
Why it matters: Code review velocity directly affects team throughput. When developers are
already in the terminal reviewing code locally, requiring a browser context switch to approve
adds enough friction that reviews are delayed. gh pr review --approve from inside the
checked-out branch — after running tests locally — removes that friction and increases the
likelihood of timely, thorough reviews.
Example 30: Mark a PR as Ready for Review
gh pr ready converts a draft pull request into a regular PR, notifying requested reviewers
and making the PR eligible for merge.
# Mark the PR for the current branch as ready for review.
gh pr ready
# => ✓ Pull request #46 is marked as ready for review.
# => (any requested reviewers are now notified)
# Mark a specific PR by number as ready.
gh pr ready 46
# => ✓ Pull request #46 is marked as ready for review.
# Convert a ready PR back to draft (for example, to block premature merge).
gh pr ready 46 --undo
# => ✓ Pull request #46 is converted to draft.Key takeaway: Use gh pr ready to promote a draft PR to review-ready state as the final
step before requesting reviews, signaling the work is complete.
Why it matters: The draft → ready transition is a formal signal in team workflows. Automating
it with a pre-push hook or CI step — for example, marking a PR ready only after all unit tests
pass locally — prevents reviewers from being notified about incomplete work. gh pr ready --undo
provides a safety valve to pull back a PR when a critical issue is discovered after review
notification.
Example 31: Check PR CI Status
gh pr checks lists all status checks (GitHub Actions workflows, external CI, code coverage
services) for a pull request, showing pass/fail status and links to logs.
# Show CI check results for the PR associated with the current branch.
gh pr checks
# => All checks were successful
# =>
# => NAME STATE ELAPSED URL
# => build pass 45s https://github.com/alice/my-project/actions/runs/123
# => test pass 2m12s https://github.com/alice/my-project/actions/runs/124
# => lint pass 18s https://github.com/alice/my-project/actions/runs/125
# => coverage/coveralls pass 31s https://coveralls.io/builds/abc123
# Check CI status for a specific PR number.
gh pr checks 45
# => (same format as above for PR #45)
# Watch CI checks in real time (polls until all checks complete).
gh pr checks --watch
# => Refreshing checks status every 10 seconds. Press Ctrl+C to quit.
# => (updates in place until all checks pass or fail)Key takeaway: gh pr checks --watch polls CI status in the terminal so you can monitor
a CI run without opening the browser Actions tab.
Why it matters: After pushing a fix for a failing test, the typical developer workflow
is to open the browser, navigate to the PR, and repeatedly refresh the Checks tab. gh pr checks --watch replaces this with a terminal-native experience, freeing the developer to
read other code while glancing at the terminal for status updates. This is particularly
valuable on slow CI pipelines where the wait time is measured in minutes.
Example 32: View PR Diff
gh pr diff displays the unified diff of a pull request's changes in the terminal, with
optional color output that works inside a pager.
# View the diff for the PR associated with the current branch.
gh pr diff
# => diff --git a/src/styles.css b/src/styles.css
# => index abc1234..def5678 100644
# => --- a/src/styles.css
# => +++ b/src/styles.css
# => @@ -45,7 +45,7 @@
# => .login-button {
# => - @media (max-width: 376px) {
# => + @media (max-width: 375px) {
# => margin-bottom: 80px;
# => }
# View the diff for a specific PR.
gh pr diff 45
# => (unified diff output for PR #45)
# View diff with no color (useful for piping into other tools).
gh pr diff --color never
# => (plain text diff without ANSI color codes)Key takeaway: gh pr diff shows the exact changes in a PR without checking out the
branch, making it fast to verify what will be merged.
Why it matters: Reviewing the diff before merging or approving is a best practice that many developers skip when the diff is accessible only through the GitHub UI. Reading the diff in the terminal, adjacent to the codebase, provides better context. Small, focused diffs reviewable in under a minute are more likely to receive thorough reviews.
Example 33: Add a Comment to a Pull Request
gh pr comment adds a general (non-review) comment to a pull request, useful for status
updates, questions, or linking related resources.
# Add a comment to the current branch's PR.
gh pr comment --body "Deployed to staging for QA verification: https://staging.example.com"
# => https://github.com/alice/my-project/pull/45#issuecomment-123456791
# Add a comment to a specific PR by number.
gh pr comment 45 --body "Tests confirmed passing on Chrome, Firefox, and Safari."
# => https://github.com/alice/my-project/pull/45#issuecomment-123456792
# Open the editor to write a longer comment.
gh pr comment 45
# => (editor opens; comment posted when file is saved and closed)Key takeaway: Use gh pr comment from deployment scripts to automatically post staging
URLs or test results back to the PR, creating a centralized record of QA activity.
Why it matters: PRs that include deployment and testing activity in their comment thread provide a complete audit trail for the feature. When a post-deploy issue is discovered, the staging URL in the PR comment lets anyone reproduce the exact build without searching through deployment logs. Automating this comment from CI eliminates the manual step that developers commonly forget.
Release Management
Example 34: Create a Release
gh release create creates a GitHub Release, uploads assets, generates release notes from
commits, and marks releases as pre-releases or latest.
# Create a release with auto-generated release notes from commits.
gh release create v1.2.0 \
--title "v1.2.0 — Mobile UI Fixes" \
--generate-notes
# => https://github.com/alice/my-project/releases/tag/v1.2.0
# => (release notes auto-generated from commit messages since last tag)
# Create a release with a specific tag, upload binary assets.
gh release create v1.2.0 \
--title "v1.2.0" \
--notes "Bug fixes and mobile improvements." \
dist/my-app-linux-amd64 dist/my-app-darwin-arm64
# => https://github.com/alice/my-project/releases/tag/v1.2.0
# => (both binary files uploaded as release assets)
# Create a pre-release (not shown as latest).
gh release create v2.0.0-beta.1 \
--title "v2.0.0-beta.1" \
--notes "Beta release for testing." \
--prerelease
# => https://github.com/alice/my-project/releases/tag/v2.0.0-beta.1Key takeaway: --generate-notes automatically creates release notes from merged PR titles
since the last release tag — useful for projects following GitHub Flow with descriptive PR names.
Why it matters: Manual release notes are a bottleneck in the release process that teams often
skip, resulting in undocumented releases. --generate-notes turns merged PR titles into a
structured changelog automatically. Combined with a conventional commits practice, this produces
release notes good enough to publish directly to users, removing one of the most tedious parts
of the release workflow.
Example 35: List and View Releases
gh release list shows all releases and pre-releases for a repository. gh release view
displays the full details of a specific release.
# List all releases for the current repository.
gh release list
# => TITLE TYPE TAG NAME PUBLISHED
# => v1.2.0 — Mobile UI Fixes latest v1.2.0 about 5 minutes ago
# => v1.1.0 — Performance latest v1.1.0 about 2 weeks ago
# => v1.0.0 — Initial release latest v1.0.0 about 1 month ago
# => v2.0.0-beta.1 pre v2.0.0-beta.1 about 1 minute ago
# View the details of a specific release.
gh release view v1.2.0
# => v1.2.0 — Mobile UI Fixes
# => Tag: v1.2.0
# => Draft: No • Pre-release: No
# =>
# => ASSETS
# => my-app-linux-amd64 (8.4 MB)
# => my-app-darwin-arm64 (7.9 MB)
# =>
# => RELEASE NOTES
# => ## What's Changed
# => * Fix login button alignment on mobile by @alice in #45Key takeaway: gh release list provides a chronological release history at a glance;
gh release view TAG shows the full release notes and downloadable assets.
Why it matters: During incident response, quickly identifying which release version a
production system is running and what changed between versions is critical. gh release list
and gh release view provide this information in seconds without navigating GitHub, enabling
faster root cause analysis when a new release causes an incident.
Example 36: Delete a Release
gh release delete removes a GitHub Release without deleting the underlying git tag, allowing
you to re-publish with corrected notes or assets.
# Delete a release by its tag name.
gh release delete v1.2.0
# => ? Delete release v1.2.0? Yes
# => ✓ Deleted release v1.2.0
# => (the git tag v1.2.0 still exists; only the GitHub Release record is removed)
# Delete without confirmation prompt (for use in automation).
gh release delete v1.2.0 --yes
# => ✓ Deleted release v1.2.0
# Delete the release AND the underlying git tag.
gh release delete v1.2.0 --cleanup-tag --yes
# => ✓ Deleted release v1.2.0
# => ✓ Deleted tag v1.2.0Key takeaway: gh release delete without --cleanup-tag removes only the release page;
--cleanup-tag also removes the git tag, which is required when a bad version must never be
re-used.
Why it matters: Accidentally publishing a release with the wrong binary or missing
assets happens. Deleting and re-creating the release corrects the mistake while keeping the
version number. Using --cleanup-tag is the right choice when a version was never actually
released to users and you want to ensure no systems accidentally pin to the bad tag.
Example 37: Download Release Assets
gh release download fetches release assets to the local filesystem, useful for install
scripts and CI pipelines that consume published binaries.
# Download all assets from the latest release.
gh release download
# => (downloads all assets from the latest release to the current directory)
# => my-app-linux-amd64 downloaded 8.4 MB
# => my-app-darwin-arm64 downloaded 7.9 MB
# Download assets for a specific release tag.
gh release download v1.2.0
# => my-app-linux-amd64 downloaded 8.4 MB
# Download only specific assets matching a pattern.
gh release download v1.2.0 --pattern "*.tar.gz"
# => (downloads only .tar.gz files from the release)
# Download to a specific directory.
gh release download v1.2.0 --dir /tmp/release-assets
# => (all assets downloaded to /tmp/release-assets/)Key takeaway: gh release download --pattern is ideal for install scripts that need
only the platform-specific binary from a multi-platform release.
Why it matters: Install scripts that rely on release assets often hard-code download URLs,
breaking whenever a new release is published. gh release download without a version tag
always fetches the latest release, making install scripts version-agnostic and automatically
correct. This is the recommended pattern for team tooling scripts that should always install
the latest approved version.
Gist Operations
Example 38: Create a Gist
gh gist create uploads files as a GitHub Gist, with options for public or secret visibility
and an optional description.
# Create a public gist from a file.
gh gist create hello.py
# => - Creating gist hello.py
# => ✓ Created public gist hello.py
# => https://gist.github.com/alice/abc123def456
# Create a secret gist with a description.
gh gist create hello.py --secret --desc "Python hello world example"
# => ✓ Created secret gist hello.py
# => https://gist.github.com/alice/xyz789ghi012
# Create a gist from stdin (pipe content directly).
echo "SELECT * FROM users LIMIT 10;" | gh gist create --filename query.sql
# => ✓ Created public gist query.sql
# => https://gist.github.com/alice/sql123abc456
# Create a gist from multiple files.
gh gist create file1.py file2.py --desc "Related Python scripts"
# => ✓ Created public gist with 2 files
# => https://gist.github.com/alice/multifile123Key takeaway: gh gist create from stdin with --filename is the fastest way to share
a snippet from the clipboard or command output without creating a file first.
Why it matters: Gists are the right tool for sharing code snippets in documentation, Slack messages, and GitHub issue comments. Creating gists from the terminal — especially piping command output or clipboard content — is faster than the GitHub UI and integrates naturally into the workflow of "write code in terminal, share it immediately."
Example 39: List and View Gists
gh gist list shows your gists with their IDs and descriptions. gh gist view displays
the content of a specific gist in the terminal.
# List your recent gists.
gh gist list
# => ID DESCRIPTION FILES VISIBILITY UPDATED
# => abc123def456 Python hello world example 1 public about 5 min ago
# => xyz789ghi012 (no description) 1 secret about 10 min ago
# => multifile123 Related Python scripts 2 public about 1 hour ago
# View a gist by ID — displays all files in the terminal.
gh gist view abc123def456
# => hello.py
# =>
# => print("Hello, World!")
# View a specific file within a multi-file gist.
gh gist view multifile123 --filename file2.py
# => (renders only file2.py from the multi-file gist)
# Open a gist in the browser.
gh gist view abc123def456 --web
# => Opening gist.github.com/alice/abc123def456 in your browser.Key takeaway: gh gist view ID is the fastest way to retrieve and display a previously
saved snippet without opening the browser.
Why it matters: Gists used as personal snippet libraries are most valuable when retrieval
is fast. Using gh gist list to find the ID and gh gist view ID to display the content
keeps the entire workflow in the terminal. Combining with shell aliases or fzf for fuzzy
search creates a powerful snippet management system.
Example 40: Edit and Delete a Gist
gh gist edit opens a gist's files in your configured editor for modification. gh gist delete permanently removes the gist.
# Edit a gist — opens the editor with the gist file content.
gh gist edit abc123def456
# => (editor opens with hello.py content; changes saved when file is closed)
# => ✓ Updated gist abc123def456
# Edit a specific file within a multi-file gist.
gh gist edit multifile123 --filename file1.py
# => (editor opens with only file1.py content)
# Delete a gist by ID.
gh gist delete abc123def456
# => ✓ Deleted gist abc123def456
# Delete without confirmation prompt.
gh gist delete abc123def456 --yes
# => ✓ Deleted gist abc123def456Key takeaway: gh gist edit updates gist content in place without requiring you to
download, edit, and re-upload — it handles the diff automatically.
Why it matters: Gists used for living documentation — configuration snippets, onboarding
commands, or reference queries — need regular updates. The terminal-native edit workflow
(gh gist edit) is faster than the browser editor and integrates with your full editor setup
including linting and syntax highlighting.
GitHub Actions Management
Example 41: List Workflow Runs
gh run list shows recent workflow runs for the current repository, with filtering by workflow,
branch, and status. It is the terminal equivalent of the Actions tab.
# List recent workflow runs for the current repository.
gh run list
# => STATUS TITLE WORKFLOW BRANCH EVENT ID ELAPSED AGE
# => ✓ Fix login button alignment on mobile CI main push 123456789 3m15s about 5 min
# => ✓ Update dependencies CI main push 123456788 2m44s about 1 hour
# => ✗ Add dark mode support CI feat/dark-mode push 123456787 1m22s about 2 hours
# Filter by workflow name.
gh run list --workflow CI
# => (shows only runs from the "CI" workflow)
# Filter by branch.
gh run list --branch feat/dark-mode
# => (shows only runs triggered on the feat/dark-mode branch)
# Filter by status (completed, in_progress, queued).
gh run list --status failure
# => (shows only failed runs)
# Limit the number of results.
gh run list --limit 5
# => (shows only the 5 most recent runs)Key takeaway: gh run list --status failure quickly surfaces recently failed workflow
runs, making it easy to identify which CI runs need investigation.
Why it matters: Monitoring CI health across branches is part of maintaining a healthy trunk.
gh run list --status failure --branch main gives an immediate picture of main branch health
without navigating the Actions tab. Piping the output to scripts enables automated failure
reports and Slack notifications.
Example 42: View a Workflow Run
gh run view displays the details of a specific workflow run including job status, step
results, and links to full logs. It is the terminal alternative to clicking a run in the UI.
# View the most recent run for the current branch.
gh run view
# => ✗ CI #123456787 (Add dark mode support) • Triggered push by alice
# => Total duration: 1m22s
# => Artifacts: 0
# =>
# => JOBS
# => ✗ build (ubuntu-latest) 1m22s
# => ✓ lint (ubuntu-latest) 18s
# View a specific run by ID.
gh run view 123456787
# => (same detail view for that specific run ID)
# View a run and display all logs (streams step-by-step output).
gh run view 123456787 --log
# => (prints the full logs for all jobs and steps)
# View logs for only failed steps.
gh run view 123456787 --log-failed
# => (shows logs only for failed steps — faster root cause analysis)Key takeaway: gh run view --log-failed filters the log to only failed steps, making
it the fastest way to diagnose a failing CI run without wading through successful step output.
Why it matters: A typical CI run produces hundreds of lines of log output. When a run
fails, the relevant error is buried in that output. --log-failed immediately shows only
the output of steps that failed, reducing triage time from minutes to seconds. This is one
of the most useful gh flags for teams with complex multi-step CI pipelines.
Example 43: Watch a Running Workflow
gh run watch streams the real-time status of an in-progress workflow run, updating the
display as jobs and steps complete.
# Watch the most recent workflow run for the current branch in real time.
gh run watch
# => JOBS
# => • build (ubuntu-latest)
# => • Set up job
# => ✓ Checkout code
# => • Run tests (running)
# => ...
# => (updates in place every few seconds)
# Watch a specific run by ID.
gh run watch 123456789
# => (watches that specific run)
# Exit with a non-zero status code if the watched run fails.
# This makes it composable in shell scripts.
gh run watch 123456789
# => Exit code: 0 (run succeeded)
# => Exit code: 1 (run failed — useful for scripted CI monitoring)Key takeaway: gh run watch provides terminal-native live feedback on CI, replacing
browser polling with an updating terminal display that exits with a meaningful status code.
Why it matters: Deployment scripts that need to wait for CI before proceeding can use
gh run watch to block until the run completes and then check the exit code. This pattern
— trigger a workflow, watch it, act on the result — is fundamental to orchestrating
multi-step automated release pipelines entirely from the command line.
Example 44: Re-run a Failed Workflow
gh run rerun re-triggers a workflow run, optionally re-running only the failed jobs to
save time and quota.
# Re-run all jobs in a failed workflow run.
gh run rerun 123456787
# => ✓ Requested rerun of run 123456787
# Re-run only the failed jobs (skips already-successful jobs).
gh run rerun 123456787 --failed
# => ✓ Requested rerun of failed jobs in run 123456787
# => (only the failing build job will be re-run; lint is skipped)
# Re-run with debug logging enabled for more verbose output.
gh run rerun 123456787 --debug
# => ✓ Requested rerun of run 123456787 with debug loggingKey takeaway: gh run rerun --failed re-runs only the jobs that failed, saving CI
minutes and shortening the feedback loop when a transient failure (network issue, flaky test)
caused the run to fail.
Why it matters: Flaky tests and transient network issues cause workflow runs to fail
without actual code problems. --failed avoids re-running the long, passing build job to
re-check a short, flaky test job. For teams that pay per CI minute, this optimization
adds up significantly over hundreds of re-runs per month.
Example 45: List and Enable/Disable Workflows
gh workflow list shows all workflows in the repository. gh workflow enable and
gh workflow disable toggle whether a workflow triggers on events.
# List all workflows in the current repository.
gh workflow list
# => NAME STATE ID
# => CI active 12345678
# => Deploy to Production active 12345679
# => Nightly Dependency Scan disabled 12345680
# Disable a workflow by ID or name (prevents future runs).
gh workflow disable "Nightly Dependency Scan"
# => ✓ Disabled workflow Nightly Dependency Scan
# Enable a previously disabled workflow.
gh workflow enable 12345680
# => ✓ Enabled workflow Nightly Dependency Scan
# View details of a specific workflow.
gh workflow view CI
# => CI - .github/workflows/ci.yml
# => ID: 12345678
# => State: active
# => (recent runs listed below)Key takeaway: Disabling workflows is the safe way to pause automation without deleting the workflow file — re-enabling requires no code changes.
Why it matters: During major refactoring or repository migrations, certain workflows may produce noise or incorrect results. Disabling them temporarily avoids failed run notifications without the risk of losing the workflow definition. This is preferable to commenting out workflow files in git, which requires a commit and affects other branches.
Example 46: Trigger a Workflow Manually
gh workflow run dispatches a workflow that has the workflow_dispatch trigger, passing
input values defined in the workflow file.
# Trigger a workflow by name with no inputs.
gh workflow run "Deploy to Production"
# => ✓ Created workflow_dispatch event for deploy.yml at main
# Trigger a workflow with input parameters.
# The input names must match those defined in the workflow's on.workflow_dispatch.inputs.
gh workflow run "Deploy to Production" \
--field environment=staging \
--field version=v1.2.0
# => ✓ Created workflow_dispatch event for deploy.yml at main
# => (workflow triggered with inputs: environment=staging, version=v1.2.0)
# Trigger a workflow on a specific branch.
gh workflow run CI --ref feat/dark-mode
# => ✓ Created workflow_dispatch event for ci.yml at feat/dark-modeKey takeaway: gh workflow run with --field replaces the GitHub UI's "Run workflow"
form, enabling scriptable manual deployment triggers from the terminal or CI scripts.
Why it matters: workflow_dispatch with typed inputs is the standard pattern for
parameterized deployments. Being able to trigger these from the terminal without navigating
to the GitHub UI is critical for on-call scenarios — for example, triggering a rollback
workflow with gh workflow run Rollback --field version=v1.1.0 during an incident is
faster and less error-prone than clicking through the browser UI.
Secrets and Variables
Example 47: Manage Repository Secrets
gh secret set creates or updates encrypted repository secrets used by GitHub Actions
workflows. Secrets are write-only from the CLI — you cannot read their values back.
# Set a secret by reading the value from stdin.
echo "my-super-secret-value" | gh secret set MY_SECRET
# => ✓ Set secret MY_SECRET for alice/my-project
# Set a secret from an environment variable (value never appears in shell history).
gh secret set MY_SECRET --body "$MY_SECRET_VALUE"
# => ✓ Set secret MY_SECRET for alice/my-project
# Set a secret for a specific repository.
gh secret set DEPLOY_KEY --repo alice/other-project --body "$DEPLOY_KEY_VALUE"
# => ✓ Set secret DEPLOY_KEY for alice/other-project
# List all secret names for the current repository (values are never shown).
gh secret list
# => NAME UPDATED
# => MY_SECRET about 5 minutes ago
# => DEPLOY_KEY about 1 month ago
# Delete a secret.
gh secret delete MY_SECRET
# => ✓ Deleted secret MY_SECRET from alice/my-projectKey takeaway: Always use --body "$VAR" or stdin piping to pass secret values — never
include secrets directly in the command to prevent exposure in shell history.
Why it matters: Secrets management is a critical security practice. Passing secrets via
stdin or environment variables keeps them out of ~/.bash_history and /proc/*/cmdline.
Automating secret rotation — for example, generating a new API key, setting it with gh secret set, and deactivating the old key — is a common operational pattern that should be
scripted rather than performed manually.
Example 48: Manage Environment Secrets
Environment secrets are scoped to a specific deployment environment (e.g., production, staging) and can have required reviewers. They provide an additional layer of access control.
# Set a secret for a specific deployment environment.
gh secret set PROD_DB_URL \
--env production \
--body "$PROD_DATABASE_URL"
# => ✓ Set secret PROD_DB_URL for alice/my-project in environment production
# Set a staging environment secret.
gh secret set STAGING_DB_URL \
--env staging \
--body "$STAGING_DATABASE_URL"
# => ✓ Set secret STAGING_DB_URL for alice/my-project in environment staging
# List secrets for a specific environment.
gh secret list --env production
# => NAME UPDATED
# => PROD_DB_URL about 5 minutes ago
# Set an organization-level secret accessible to selected repositories.
gh secret set ORG_DEPLOY_KEY \
--org myorg \
--visibility selected \
--repos my-project,other-project \
--body "$DEPLOY_KEY"
# => ✓ Set secret ORG_DEPLOY_KEY for myorgKey takeaway: Scope secrets to environments for production access control — environment secrets can require manual approval before deployment workflows can access them.
Why it matters: Repository-level secrets are accessible to all workflows in the repo. Environment secrets for production add an approval gate, preventing automated workflows from accidentally deploying to production or accessing production credentials without human review. This is a critical security control for regulated environments and fintech applications.
Example 49: Manage Variables
Variables are non-secret configuration values used in workflows, similar to secrets but readable. They are appropriate for environment names, feature flags, and non-sensitive config.
# Set a repository variable.
gh variable set APP_ENV --body "production"
# => ✓ Set variable APP_ENV for alice/my-project
# Set a variable for a specific environment.
gh variable set MAX_REPLICAS --env production --body "10"
# => ✓ Set variable MAX_REPLICAS for alice/my-project in environment production
# List all variables for the current repository.
gh variable list
# => NAME VALUE UPDATED
# => APP_ENV production about 5 minutes ago
# List variables for a specific environment.
gh variable list --env production
# => NAME VALUE UPDATED
# => MAX_REPLICAS 10 about 5 minutes ago
# Delete a variable.
gh variable delete APP_ENV
# => ✓ Deleted variable APP_ENV from alice/my-projectKey takeaway: Use variables for non-sensitive configuration like environment names, replica counts, and feature flags — use secrets only for sensitive values like passwords and tokens.
Why it matters: Mixing secrets and non-secret configuration in the secrets store makes auditing harder and complicates rotation workflows. Variables are readable and auditable, making it easy to verify the current configuration state of a workflow without decryption. Separating the two categories also makes access control cleaner.
Label Management
Example 50: Create and List Labels
gh label create adds a new label to a repository's label set. gh label list shows all
existing labels with their colors and descriptions.
# Create a new label with a color and description.
gh label create "performance" \
--color "#0173B2" \
--description "Performance-related issues and improvements"
# => ✓ Label "performance" created in alice/my-project
# List all labels for the current repository.
gh label list
# => NAME DESCRIPTION COLOR
# => bug Something isn't working #d73a4a
# => documentation Improvements or additions to docs #0075ca
# => performance Performance-related issues and improvements #0173B2
# List labels for a specific repository.
gh label list --repo cli/cli
# => (lists all labels in the cli/cli repository)Key takeaway: Create labels with semantic colors — use the accessible color palette for consistency with your documentation and diagrams.
Why it matters: A consistent, well-defined label taxonomy is the foundation of issue
triage and sprint planning. Labels with clear colors and descriptions reduce ambiguity about
where an issue belongs. Scripting label creation with gh label create enables label set
synchronization across multiple repositories in an organization, ensuring consistent triage
workflows everywhere.
Example 51: Edit and Delete Labels
gh label edit updates a label's name, color, or description. gh label delete removes
a label from the repository.
# Edit an existing label's color and description.
gh label edit "performance" \
--color "#029E73" \
--description "Performance optimization issues"
# => ✓ Label "performance" edited in alice/my-project
# Rename a label.
gh label edit "performance" --name "perf"
# => ✓ Label "perf" edited in alice/my-project
# Delete a label.
gh label delete "perf"
# => ✓ Label "perf" deleted from alice/my-project
# Delete without confirmation prompt.
gh label delete "perf" --yes
# => ✓ Label "perf" deleted from alice/my-projectKey takeaway: Renaming labels with gh label edit --name updates the label name in all
existing issues and PRs automatically — no manual re-labeling required.
Why it matters: Label taxonomy evolves as teams refine their triage process. Being able to rename, re-color, and delete labels while preserving their association with existing issues prevents orphaned labels and maintains clean issue history. Scripting label set synchronization across repositories ensures that label updates propagate everywhere in the organization.
SSH Keys and Codespaces
Example 52: Add and List SSH Keys
gh ssh-key add uploads an SSH public key to your GitHub account, enabling SSH-based git
operations. gh ssh-key list shows all registered keys.
# Add a new SSH public key to your GitHub account.
# Reads the key from the file; only the public key is uploaded.
gh ssh-key add ~/.ssh/id_ed25519.pub --title "Work Laptop 2026"
# => ✓ Public key added to your account
# List all SSH keys registered on your account.
gh ssh-key list
# => TITLE ID KEY ADDED
# => Work Laptop 2026 8675309 SHA256:... about 5 minutes ago
# => Old MacBook Pro 1234567 SHA256:... about 2 years ago
# Delete an SSH key by ID.
gh ssh-key delete 1234567
# => ✓ SSH key 1234567 (Old MacBook Pro) deleted from your accountKey takeaway: gh ssh-key add is the terminal-native way to register a new machine's
SSH key — no browser navigation to Settings → SSH Keys required.
Why it matters: New developer onboarding includes generating an SSH key and adding it to
GitHub. Automating this step with gh ssh-key add as part of an onboarding script reduces
setup friction and ensures the key is correctly registered before any git operations are
attempted. Regular SSH key rotation is also automatable using gh ssh-key delete and gh ssh-key add in sequence.
Example 53: Codespace: Create and List
gh codespace create provisions a GitHub Codespace for a repository, and gh codespace list
shows all your active and stopped Codespaces.
# Create a Codespace for the current repository (interactive machine selection).
gh codespace create
# => ? Choose a machine type: 2-core, 8GB RAM, 32GB storage
# => ✓ Codespace created: alice-my-project-abc123
# => (Codespace is provisioning and will be ready in ~30 seconds)
# Create a Codespace non-interactively with a specific machine type and branch.
gh codespace create \
--repo alice/my-project \
--branch feat/dark-mode \
--machine basicLinux32gb
# => ✓ Codespace created: alice-my-project-xyz789
# List all Codespaces for your account.
gh codespace list
# => NAME DISPLAY NAME REPOSITORY BRANCH STATE CREATED
# => alice-my-project-abc123 my-project alice/my-project main Active about 5 min ago
# => alice-my-project-xyz789 my-project alice/my-project feat/dark-mode Stopped about 2 days agoKey takeaway: gh codespace create --repo --branch --machine creates a fully provisioned
development environment in the cloud, specifiable entirely from the terminal.
Why it matters: Codespaces enable consistent development environments that match production
configuration without local setup. Creating a Codespace for a specific PR branch allows
reviewers to test code changes in a clean environment without affecting their local setup.
This is particularly valuable for reviewing PRs that change development environment configuration
files like devcontainer.json.
Example 54: SSH into a Codespace
gh codespace ssh opens an SSH session into a running Codespace, providing terminal access
to the remote development environment.
# SSH into the most recently used Codespace (prompts if multiple exist).
gh codespace ssh
# => ? Choose a codespace: alice-my-project-abc123 (main)
# => Welcome to Ubuntu 20.04.6 LTS
# => (now inside the remote Codespace environment)
# SSH into a specific Codespace by name.
gh codespace ssh --codespace alice-my-project-abc123
# => Welcome to Ubuntu 20.04.6 LTS
# => (connected to the named Codespace)
# Run a single command in a Codespace without an interactive session.
gh codespace ssh --codespace alice-my-project-abc123 -- "npm test"
# => > my-project@1.2.0 test
# => > vitest run
# => ✓ 42 tests passKey takeaway: gh codespace ssh -- "command" runs a single command remotely in a
Codespace, enabling CI-like testing in a pre-configured cloud environment from the terminal.
Why it matters: When a bug only reproduces in an environment with specific dependencies
or OS versions, gh codespace ssh provides access to that environment without complex local
VM setup. Running tests in the Codespace with -- "npm test" verifies that the code works
in the standardized team environment, not just on your local machine configuration.
Example 55: Stop and Delete a Codespace
gh codespace stop suspends a Codespace to preserve its state while stopping billing.
gh codespace delete permanently removes it.
# Stop a running Codespace (suspends it, preserves state).
gh codespace stop --codespace alice-my-project-abc123
# => ✓ Codespace alice-my-project-abc123 stopped
# Stop all Codespaces (useful for end-of-day cleanup).
gh codespace stop --all
# => ✓ Codespace alice-my-project-abc123 stopped
# => ✓ Codespace alice-my-project-xyz789 stopped
# Delete a stopped Codespace permanently.
gh codespace delete --codespace alice-my-project-xyz789
# => ✓ Codespace alice-my-project-xyz789 deleted
# Delete all stopped Codespaces older than 7 days.
gh codespace delete --days 7 --all
# => ✓ Deleted 3 codespaces older than 7 daysKey takeaway: Stop Codespaces when not in use to pause billing; delete them when the work is complete and changes have been committed and pushed.
Why it matters: Forgotten running Codespaces accrue billing charges. Establishing a habit
of stopping Codespaces at the end of each session, and deleting them after merging the
associated PR, keeps Codespace costs predictable. gh codespace stop --all is a good end-of-day
shell alias that ensures no idle Codespaces run overnight.
Example 56: Port Forwarding in a Codespace
gh codespace ports forward exposes a port running inside a Codespace to your local machine,
allowing you to access web servers, databases, and other services running in the cloud environment.
# Forward Codespace port 3000 to local port 3000.
gh codespace ports forward 3000:3000 --codespace alice-my-project-abc123
# => Forwarding ports: ...
# => 3000 -> localhost:3000
# => (local browser can now access http://localhost:3000 which serves from the Codespace)
# Forward multiple ports simultaneously.
gh codespace ports forward 3000:3000 5432:5432 --codespace alice-my-project-abc123
# => 3000 -> localhost:3000 (Next.js dev server)
# => 5432 -> localhost:5432 (PostgreSQL in the Codespace)
# List open ports in a Codespace.
gh codespace ports --codespace alice-my-project-abc123
# => LABEL PORT VISIBILITY BROWSE URL
# => nextjs 3000 private https://alice-my-project-abc123-3000.app.github.dev
# => postgres 5432 private (not browsable)Key takeaway: Port forwarding bridges your local browser to services running in a Codespace, enabling local browser testing of code that runs in the remote cloud environment.
Why it matters: Full-stack development in a Codespace requires access to both the frontend and database services. Port forwarding makes this transparent — the developer experience in the local browser is identical to local development, but the code runs in the controlled, consistent Codespace environment. This is the key feature that makes Codespaces a complete local development replacement rather than just a code editor.
Last updated March 31, 2026