Recently I was using Claude Code and the agent decided to close two issues I was referencing on one of our public repos, as it had access to the gh command, which I hadn’t installed previously. The specific action was fine, but I realised the agent actually had full access to my Github, including the ability to author new tokens. This is a big risk for both prompt injection and also simple mistakes that a subagent could make.
To solve this I’ve created a restricted read-only token, put a read/write token in my OSX keychain, and created gitw and ghw wrapper scripts that requires sudo and have 0-second timeout for passwordless authentication. This means that my coding agent cannot git push or make write actions using gh unless it can execute a privilege-escalation attack, steal my root password, or trick me into typing my root password into some other command. If someone is able to do full agent take-over, stealing my root password is unfortunately probably not too difficult. But for prompt injection it’s probably infeasible.
Prompt-injection supply-chain attacks keep me up at night, and if you’re using coding agents you should worry about them too. So far Anthropic, OpenAI, Google etc have been doing well with their guardrails, but if at any point someone does develop a generalised attack, the 0-day will spread like wildfire as infected agents will be able to upload the attack text to READMEs, websites, social media, email, messengers…
You should also take make sure you’re using a firewall, and avoid letting secrets rest on disk in plain text. It is relatively easy for a prompt injection attack to convince an agent to run python -m http.server -p 8888. If a website is able to get your IP and serve your agent the attack, it can immediately scan the port and try to download your environment files or other secrets.
The following step-by-step instructions were generated with AI assistance, as I suck at writing this sort of instructional text. Please let me know if any of the steps don’t work or if there’s a problematic oversight or mistake.
Step-by-step (LLM generated)
- Store your current write PAT in the OS keychain
- Create a
ghwwrapper that fetches the token from the keychain viasudo - Create a
gitwwrapper forgit pushover HTTPS using the same token - Disable sudo credential caching for both wrappers
- Replace
gh’s auth with a read-only PAT - Block
ghw,gitw, andsudoin your agent’s tool policy
Step 1: Store the write token in the keychain
If your current gh is authenticated with a write-capable PAT, pipe it directly into the keychain. This avoids the token appearing on disk at any point.
macOS:
read -r TOKEN < <(gh auth token) && security add-generic-password -s "github-write-pat" -a "$(whoami)" -w "$TOKEN" && unset TOKEN
Linux (GNOME Keyring):
read -r TOKEN < <(gh auth token) && echo -n "$TOKEN" | secret-tool store --label="GitHub Write PAT" service github-write-pat user "$(whoami)" && unset TOKEN
read is a shell builtin, so the token never appears in process arguments. The security / secret-tool invocation does briefly expose it in ps, but this is a one-time local setup step — not a meaningful risk.
Verify the token is stored:
# macOSsecurity find-generic-password -s "github-write-pat" -a "$(whoami)" -w
# Linuxsecret-tool lookup service github-write-pat user "$(whoami)"
Step 2: Create the ghw wrapper
This script requires sudo, fetches the write token from the keychain at invocation time, and passes it to gh. The script itself contains no secrets.
sudo tee /usr/local/bin/ghw > /dev/null << 'EOF'#!/bin/shif [ "$EUID" -ne 0 ]; then echo "ghw requires sudo" >&2 exit 1fi
case "$(uname -s)" in Darwin) TOKEN=$(security find-generic-password -s "github-write-pat" -a "$SUDO_USER" -w 2>/dev/null) ;; Linux) TOKEN=$(secret-tool lookup service github-write-pat user "$SUDO_USER") ;; *) echo "Unsupported platform" >&2 exit 1 ;;esac
if [ -z "$TOKEN" ]; then echo "No GitHub write token found in keychain" >&2 exit 1fi
GITHUB_TOKEN="$TOKEN" exec gh "$@"EOFsudo chmod 755 /usr/local/bin/ghw
The shebang is #!/bin/sh, not #!/usr/bin/env sh. For a sudo-gated script, you want a fixed path — env would resolve sh via $PATH, which an attacker or misconfigured agent could manipulate. The script uses only POSIX features, so /bin/sh works on both macOS and Linux.
$SUDO_USER is used instead of $USER because inside sudo, $USER is root. $SUDO_USER is your actual username — the one that owns the keychain entry.
Linux note: secret-tool requires access to the user’s D-Bus session, which can be lost under sudo. If this is a problem, you may need to preserve DBUS_SESSION_BUS_ADDRESS in your sudoers config, or use pass (GPG-backed) as an alternative secret store.
Test it:
sudo ghw auth status
This should show your write token’s scopes.
Step 3: Create the gitw wrapper
The read-only PAT on gh also restricts git push, because gh registers itself as git’s credential helper. This is good — the agent can’t push. But it means you can’t push either. Rather than switching to SSH, you can use the same pattern: a sudo-gated wrapper that injects the write token for git operations.
sudo tee /usr/local/bin/gitw > /dev/null << 'EOF'#!/bin/shif [ "$EUID" -ne 0 ]; then echo "gitw requires sudo" >&2 exit 1fi
case "$(uname -s)" in Darwin) TOKEN=$(security find-generic-password -s "github-write-pat" -a "$SUDO_USER" -w 2>/dev/null) ;; Linux) TOKEN=$(secret-tool lookup service github-write-pat user "$SUDO_USER") ;; *) echo "Unsupported platform" >&2 exit 1 ;;esac
if [ -z "$TOKEN" ]; then echo "No GitHub write token found in keychain" >&2 exit 1fi
git -c "credential.https://github.com.helper=" \ -c "credential.https://github.com.helper=!f() { echo username=x-access-token; echo password=$TOKEN; }; f" \ "$@"EOFsudo chmod 755 /usr/local/bin/gitw
The key detail is the credential helper override. gh registers a host-specific credential helper (credential.https://github.com.helper), which takes priority over the generic credential.helper. The first -c clears that host-specific config, and the second sets our token-injecting helper.
Test it:
sudo gitw push origin main
Step 4: Disable sudo credential caching
By default, sudo caches your password for a few minutes. This is fine for routine commands but defeats the purpose here — an agent that can run sudo ghw during the cache window would succeed without a password prompt.
sudo visudo -f /etc/sudoers.d/ghw
Add:
Defaults!/usr/local/bin/ghw timestamp_timeout=0Defaults!/usr/local/bin/gitw timestamp_timeout=0
This forces a password prompt on every sudo ghw or sudo gitw invocation without affecting your other sudo usage.
Step 5: Replace gh auth with a read-only PAT
Log out of gh:
gh auth logout
Create a fine-grained PAT on GitHub (Settings → Developer settings → Fine-grained personal access tokens) with only:
- Contents: Read
- Metadata: Read
This step can’t be automated — GitHub’s API doesn’t support creating fine-grained PATs programmatically.
Log in with the new token:
read -rs TOKEN && echo "$TOKEN" | gh auth login --with-token && unset TOKEN
read -rs reads from stdin with no echo, so the token doesn’t appear in your terminal or shell history.
Step 6: Configure your agent
Block access to the write paths in your agent’s tool policy. The specifics depend on your agent:
Claude Code (.claude/settings.json):
Add ghw, gitw, and sudo to your denied commands or tools list.
General approach:
If your agent runs in a container or restricted shell, simply don’t mount ghw on its $PATH. If it runs as your user, rely on tool policy plus the sudo password gate as defense in depth.
Verify the split
# Read-only — should workgh repo view your-org/some-repo
# Write via gh — should failgh pr create --title "test" --body "test"
# Write via gh with sudo — should work (and prompt for password)sudo ghw pr create --title "test" --body "test"
# Push — should failgit push origin main
# Push with sudo — should work (and prompt for password)sudo gitw push origin main
Security properties
What’s protected: The write token never exists on disk. It lives in the OS keychain, which requires user-session authentication. The ghw and gitw scripts contain no secrets. sudo creates an OS-level privilege boundary. The agent, running as a normal user, cannot read the token from /proc/<pid>/environ because the wrapper processes run as root.
What’s not: This is defense in depth, not a sandbox. If the agent can somehow obtain your sudo password, the boundary fails. If you run the agent as root (don’t), the boundary fails. The read-only PAT can still be exfiltrated — it just limits the blast radius to read access on the repos it’s scoped to.
The strongest variant is to run the agent as a dedicated low-privilege user with no sudo access and no keychain session. That turns the guardrails into hard walls at the OS level.