---
name: github-auth
description: "GitHub auth setup: HTTPS tokens, SSH keys, gh CLI login."
version: 1.1.0
author: Hermes Agent
license: MIT
metadata:
  hermes:
    tags: [GitHub, Authentication, Git, gh-cli, SSH, Setup]
    related_skills: [github-pr-workflow, github-code-review, github-issues, github-repo-management]
---

# GitHub Authentication Setup

This skill sets up authentication so the agent can work with GitHub repositories, PRs, issues, and CI. It covers two paths:

- **`git` (always available)** — uses HTTPS personal access tokens or SSH keys
- **`gh` CLI (if installed)** — richer GitHub API access with a simpler auth flow

## Detection Flow

When a user asks you to work with GitHub, run this check first:

```bash
# Check what's available
git --version
gh --version 2>/dev/null || echo "gh not installed"

# Check if already authenticated
gh auth status 2>/dev/null || echo "gh not authenticated"
git config --global credential.helper 2>/dev/null || echo "no git credential helper"
```

**Decision tree:**
1. If `gh auth status` shows authenticated → you're good, use `gh` for everything
2. If `gh` is installed but not authenticated → use "gh auth" method below
3. If `gh` is not installed → use "git-only" method below (no sudo needed)

---

## Token Types: Classic vs. Fine-Grained

GitHub offers two PAT types with different permission models.

### Classic PAT (`ghp_...`)

Uses OAuth scopes — simple, coarse-grained.

| Scope | Grants |
|-------|--------|
| `repo` | Full private repo access (push/pull/issues/PRs) |
| `workflow` | GitHub Actions management |
| `read:org` | Read org membership (needed for org repos) |

**Recommended for Hermes Agent:** Classic PAT with `repo` + `workflow` scopes.
Generate at: **https://github.com/settings/tokens** → "Generate new token (classic)"

### Fine-Grained PAT (`github_pat_...`)

Uses granular permissions per repository or organization. **Scopes are NOT listed in API response headers.** Permissions are set in the GitHub UI and control access at the permission level.

**Key permission locations (not obvious):**

| Permission | Where to set it | Needed for |
|------------|----------------|------------|
| **Repository creation** | Account permissions (not repository-level!) | `gh repo create` |
| **Contents: read/write** | Repository permissions | Push code, create/update files |
| **Metadata: read** | Repository permissions (granted by default) | List repos, view details |
| **Administration: read/write** | Repository permissions | Rename/delete repos, change settings |
| **Pull requests: read/write** | Repository permissions | Create/manage PRs |

**To update a fine-grained PAT's permissions:**
GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens → Click the token → scroll to:
- **Account permissions** section → Repository creation → Write
- **Repository permissions** section → Contents → Read and write

**Detection:** Check token type by looking at the prefix:
```bash
TOKEN_PREFIX=$(echo "$TOKEN" | cut -c1-10)
if [[ "$TOKEN_PREFIX" == github_pat_* ]]; then
  echo "Fine-grained PAT — uses permissions model"
else
  echo "Classic PAT — uses OAuth scopes"
fi
```

**Limitation:** Fine-grained PATs cannot manage SSH keys via the API (`POST /user/keys` returns 403).

### Method 1: Git-Only Authentication (No gh, No sudo)

This works on any machine with `git` installed. No root access needed.

### Option A: HTTPS with Personal Access Token (Recommended)

This is the most portable method — works everywhere, no SSH config needed.

**Step 1: Create a personal access token**

See the token type guide above. Classic PAT (`ghp_...`) is simpler for most cases.

For a classic PAT:
- Go to **https://github.com/settings/tokens**
- Click "Generate new token (classic)"
- Give it a name like "hermes-agent"
- Select scopes: `repo`, `workflow`, `read:org`
- Set expiration (90 days is a good default)
- Copy the token

**Step 2: Configure git to store the token**

```bash
# Set up the credential helper to cache credentials
# "store" saves to ~/.git-credentials in plaintext (simple, persistent)
git config --global credential.helper store

# Now do a test operation that triggers auth — git will prompt for credentials
# Username: <their-github-username>
# Password: <paste the personal access token, NOT their GitHub password>
git ls-remote https://github.com/<their-username>/<any-repo>.git
```

After entering credentials once, they're saved and reused for all future operations.

**Alternative: cache helper (credentials expire from memory)**

```bash
# Cache in memory for 8 hours (28800 seconds) instead of saving to disk
git config --global credential.helper 'cache --timeout=28800'
```

**Alternative: set the token directly in the remote URL (per-repo)**

```bash
# Embed token in the remote URL (avoids credential prompts entirely)
git remote set-url origin https://<username>:<token>@github.com/<owner>/<repo>.git
```

**Step 3: Configure git identity**

```bash
# Required for commits — set name and email
git config --global user.name "Their Name"
git config --global user.email "their-email@example.com"
```

**Step 4: Verify**

```bash
# Test push access (this should work without any prompts now)
git ls-remote https://github.com/<their-username>/<any-repo>.git

# Verify identity
git config --global user.name
git config --global user.email
```

### Option B: SSH Key Authentication

Good for users who prefer SSH or already have keys set up.

**Step 1: Check for existing SSH keys**

```bash
ls -la ~/.ssh/id_*.pub 2>/dev/null || echo "No SSH keys found"
```

**Step 2: Generate a key if needed**

```bash
# Generate an ed25519 key (modern, secure, fast)
ssh-keygen -t ed25519 -C "their-email@example.com" -f ~/.ssh/id_ed25519 -N ""

# Display the public key for them to add to GitHub
cat ~/.ssh/id_ed25519.pub
```

Tell the user to add the public key at: **https://github.com/settings/keys**
- Click "New SSH key"
- Paste the public key content
- Give it a title like "hermes-agent-<machine-name>"

**Step 3: Test the connection**

```bash
ssh -T git@github.com
# Expected: "Hi <username>! You've successfully authenticated..."
```

**Step 4: Configure git to use SSH for GitHub**

```bash
# Rewrite HTTPS GitHub URLs to SSH automatically
git config --global url."git@github.com:".insteadOf "https://github.com/"
```

**Step 5: Configure git identity**

```bash
git config --global user.name "Their Name"
git config --global user.email "their-email@example.com"
```

---

## Method 2: gh CLI Authentication

If `gh` is installed, it handles both API access and git credentials in one step.

### Interactive Browser Login (Desktop)

```bash
gh auth login
# Select: GitHub.com
# Select: HTTPS
# Authenticate via browser
```

### Token-Based Login (Headless / SSH Servers)

```bash
echo "<THEIR_TOKEN>" | gh auth login --with-token

# Set up git credentials through gh
gh auth setup-git
```

### Verify

```bash
gh auth status
```

---

## Using the GitHub API Without gh

When `gh` is not available, you can still access the full GitHub API using `curl` with a personal access token. This is how the other GitHub skills implement their fallbacks.

### Setting the Token for API Calls

```bash
# Option 1: Export as env var (preferred — keeps it out of commands)
export GITHUB_TOKEN="<token>"

# Then use in curl calls:
curl -s -H "Authorization: token $GITHUB_TOKEN" \
  https://api.github.com/user
```

### Extracting the Token from Git Credentials

If git credentials are already configured (via credential.helper store), the token can be extracted:

```bash
# Read from git credential store
grep "github.com" ~/.git-credentials 2>/dev/null | head -1 | sed 's|https://[^:]*:\([^@]*\)@.*|\1|'
```

### Helper: Detect Auth Method

Use this pattern at the start of any GitHub workflow:

```bash
# Try gh first, fall back to git + curl
if command -v gh &>/dev/null && gh auth status &>/dev/null; then
  echo "AUTH_METHOD=gh"
elif [ -n "$GITHUB_TOKEN" ]; then
  echo "AUTH_METHOD=curl"
elif [ -f ~/.hermes/.env ] && grep -q "^GITHUB_TOKEN=" ~/.hermes/.env; then
  export GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" ~/.hermes/.env | head -1 | cut -d= -f2 | tr -d '\n\r')
  echo "AUTH_METHOD=curl"
elif grep -q "github.com" ~/.git-credentials 2>/dev/null; then
  export GITHUB_TOKEN=$(grep "github.com" ~/.git-credentials | head -1 | sed 's|https://[^:]*:\([^@]*\)@.*|\1|')
  echo "AUTH_METHOD=curl"
else
  echo "AUTH_METHOD=none"
  echo "Need to set up authentication first"
fi
```

---

## Network-Restricted Environments (China / Firewalled Servers)

When running from servers in mainland China or other restricted networks, GitHub's `git` protocol (port 443) may be throttled or blocked, while the REST API (`api.github.com`) often works fine.

### Symptom Detection

```bash
# git protocol fails (times out)
git ls-remote https://github.com/owner/repo.git  # ❌ times out after 15-30s
git push origin main                              # ❌ times out

# REST API works (fast response)
curl -s -o /dev/null -w '%{http_code} %{time_total}s' https://api.github.com
# → "200 0.27s"  ✅ Works fine

# gh CLI may work partially (uses api.github.com)
gh auth status  # ✅ Works
gh repo view    # ✅ Works
gh repo create  # ❌ may fail if token lacks create permissions
```

### Workaround: Push via GitHub Contents API

When `git push` times out but `api.github.com` is reachable, use the REST API to push files one at a time:

```python
import urllib.request, json, base64

token = "<GITHUB_TOKEN>"
headers = {
    "Authorization": f"Bearer {token}",
    "Accept": "application/vnd.github+json",
    "Content-Type": "application/json"
}
base_url = f"https://api.github.com/repos/{owner}/{repo}"

# Push each file via Contents API
for filepath in files_to_push:
    with open(local_path, 'rb') as f:
        content = f.read()
    encoded = base64.b64encode(content).decode()
    
    data = json.dumps({
        "message": f"Add {filepath}",
        "content": encoded,
        "branch": "main"
    }).encode()
    
    req = urllib.request.Request(
        f"{base_url}/contents/{filepath}",
        data=data, headers=headers, method="PUT"
    )
    resp = urllib.request.urlopen(req)
```

**Caveats:**
- Slower than `git push` (one file at a time)
- Can't push symlinks directly — use the actual file content
- Token needs **Contents: write** permission (fine-grained PAT) or `repo` scope (classic PAT)
- If there are many files, batch them in groups

### SSH Alternative

If HTTPS git is blocked but SSH port 22 is open:

```bash
# Generate a key
ssh-keygen -t ed25519 -f ~/.ssh/github_hermes -N ""

# Show the public key — user must add at https://github.com/settings/keys
cat ~/.ssh/github_hermes.pub

# After key is added, push via SSH
git remote set-url origin git@github.com:owner/repo.git
git push origin main
```

**Note:** Fine-grained PATs cannot add SSH keys via the API. The user must add them manually through the GitHub web UI.

### Non-Interactive Shell Quirks

Background processes (`terminal(background=true)`) and non-interactive shells may not inherit environment variables or have `$HOME` set. Workarounds:

```bash
# 1. Embed token directly in remote URL (per-repo)
git remote set-url origin https://user:${TOKEN}@github.com/owner/repo.git

# 2. Explicitly set HOME in non-interactive commands
export HOME=/home/ubuntu

# 3. Use a persistent token file
echo "https://user:${TOKEN}@github.com" > /home/ubuntu/.git-credentials
chmod 600 /home/ubuntu/.git-credentials
```

## Troubleshooting

| Problem | Solution |
|---------|----------|
| `git push` asks for password | GitHub disabled password auth. Use a personal access token as the password, or switch to SSH |
| `remote: Permission to X denied` | Token may lack `repo` scope — regenerate with correct scopes |
| `fatal: Authentication failed` | Cached credentials may be stale — run `git credential reject` then re-authenticate |
| `ssh: connect to host github.com port 22: Connection refused` | Try SSH over HTTPS port: add `Host github.com` with `Port 443` and `Hostname ssh.github.com` to `~/.ssh/config` |
| Credentials not persisting | Check `git config --global credential.helper` — must be `store` or `cache` |
| Multiple GitHub accounts | Use SSH with different keys per host alias in `~/.ssh/config`, or per-repo credential URLs |
| `gh: command not found` + no sudo | Use git-only Method 1 above — no installation needed |
