DEV Community

Cover image for GitHub's Own Cloud Agent Can't Run git push. Yours Can Push to main
Gabriel Anhaia
Gabriel Anhaia

Posted on AI-assisted

GitHub's Own Cloud Agent Can't Run git push. Yours Can Push to main


GitHub owns the server your code lives on. It also builds a coding agent that runs on that server. And its own risk page for that agent says this, in plain words: "Copilot cloud agent can only perform simple push operations. It cannot directly run git push or other Git commands."

Now look at the agent in your terminal. It runs as you. It has your SSH key, your git credential helper and your gh login. If nothing else stands in the way, git push origin main from that agent is the same command as git push origin main from you, and to GitHub it's the same push from the same account.

If the company that runs the repo host won't let its own cloud agent push wherever it likes, a CLI agent on your laptop shouldn't get to either. Landing work on main is a human step. Below are three controls that make it one, and where they leak.

What GitHub won't let its own agent do

Copilot cloud agent is the product GitHub used to call Copilot coding agent (GitHub changelog). Its risks and mitigations page lists the fences GitHub put around it:

  • Only users with write access to the repository can trigger it.
  • It gets one branch. Mention @copilot on an existing pull request and it can write to that pull request's branch. Otherwise it gets a new copilot/ branch and can only push there.
  • Its credentials are limited: simple push operations, no direct git push, no other Git commands.
  • It is "subject to any branch protections and required checks for the working repository."
  • It can't mark its pull requests "Ready for review," and it can't approve or merge one.
  • By default, GitHub Actions workflows don't run on its code until someone with write access clicks Approve and run workflows. You can configure Copilot to allow automatic runs, but that's a choice you make.
  • The person who asked for the pull request can't approve it.

Count them. Seven separate limits, from the vendor that controls both the agent and the server it pushes to. If any vendor had a reason to give its agent the run of the repo, it was this one. It fenced the agent in instead.

Your agent pushes as you

GitHub ships a local agent too. Copilot CLI runs on your machine, and its docs warn that "a shell command can do anything your user account can do." Their deny example is a flag you add yourself: --deny-tool='shell(git push)'. Once the agent runs as you, the fence is your job.

Take Claude Code as the worked example, since its docs are explicit. In the default (Manual) mode it asks before running a Bash command, outside a built-in set of read-only ones. That's a real gate. It also has three ways to open:

  • Answer "Yes, and don't ask again" to a Bash command once, and the approval is permanent for that repository and command.
  • In auto mode, a classifier reviews actions instead of you.
  • In bypassPermissions mode the prompts are skipped, and the docs say to use it "only" in "isolated environments like containers or VMs."

And the line that should end the "I told it never to push" debate, from the same page: "Permission rules are enforced by Claude Code, not by the model. Instructions in your prompt or CLAUDE.md shape what Claude tries to do, but they don't change what Claude Code allows." A sentence in CLAUDE.md is a request. Your SSH key doesn't read it.

The fix is the one GitHub used for its cloud agent: decide where the agent may push, and enforce it somewhere the agent doesn't control. Here are three layers, from weakest to strongest.

Control 1: a deny rule in Claude Code

Put this in .claude/settings.json to cover one repo, or in ~/.claude/settings.json to cover every project:

{
  "permissions": {
    "deny": [
      "Bash(git push *)",
      "Bash(gh pr merge *)"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Claude Code evaluates deny first, then ask, then allow, and a deny rule from any settings scope beats an allow rule from any other (permissions docs). A trailing * also matches the bare command, so plain git push is covered. If you do want the agent to push its own branch, move the rule from deny to ask and you'll get a prompt every time.

Anthropic's docs name the gap themselves. A Bash rule matches the command text Claude writes. Bash(git push *) stops git push origin main. It does not stop git -C . push origin main, git -c push.default=current push origin main or git 'push' origin main. In the docs' words, a deny rule "isn't a security boundary around the program." It catches the push the agent usually writes. That's worth having, and it's not enough.

Control 2: a pre-push hook that only lets agent branches out

Git runs the pre-push hook for every push, however the command was spelled. Per the githooks docs, the hook gets one line per ref on standard input, in the form <local-ref> <local-sha> <remote-ref> <remote-sha>, and a non-zero exit aborts the push "without pushing anything."

This one lets a push through only when every target ref is a branch under agent/ or hermes/:

#!/bin/sh
# pre-push: from this clone, only agent branches go out
allowed="agent/ hermes/"

while read -r local_ref local_sha remote_ref remote_sha
do
  ok=no
  for prefix in $allowed; do
    case "$remote_ref" in
      refs/heads/"$prefix"*) ok=yes ;;
    esac
  done
  if [ "$ok" = no ]; then
    echo "pre-push: blocked push to $remote_ref" >&2
    echo "Push an agent/ branch and open a PR." >&2
    exit 1
  fi
done
exit 0
Enter fullscreen mode Exit fullscreen mode

Tags, main, and deleting main on the remote all fail the prefix check. Add your own prefix to allowed if you push feature branches from the same clone.

Install it where Git actually looks. The git-worktree docs say not to guess which .git folder a path belongs to and to ask git rev-parse --git-path instead:

hooks="$(git rev-parse --git-path hooks)"
cp pre-push "$hooks/pre-push"
chmod +x "$hooks/pre-push"
Enter fullscreen mode Exit fullscreen mode

Where it leaks: git push --no-verify skips the hook "completely" (git-push docs), and so does an agent that rewrites the hook. For the second one, Claude Code puts .git on its protected paths list: in default and acceptEdits modes a write there prompts you, while bypassPermissions allows it. The hook raises the cost of a mistake. It still runs on the agent's machine, as the agent's user.

Control 3: a ruleset on main

The last layer lives on the server, where no file on your laptop can change it. In your repository, open Settings, then Rulesets, then New ruleset and New branch ruleset. Set enforcement to Active, add the default branch as the target, and pick your rules (GitHub docs). From GitHub's list of available rules:

  • Require a pull request before merging, so every change to main is tied to a pull request.
  • Inside that rule, required approvals set to at least 1.
  • Block force pushes, which is on by default.
  • Restrict deletions, also on by default, so only users with bypass permission can delete the branch.

The approvals setting is the one that matters. On its own, the pull request rule only means a pull request exists: GitHub's page says it "doesn't necessarily have to be approved, but it must be opened." An agent with your gh login can open that pull request and merge it, and the only thing in its way is Control 1's gh pr merge deny, which is a text match. With approvals required, the agent still opens the pull request as you, and pull request authors cannot approve their own pull requests. Someone else has to.

If you're the only human on the repo, that blocks you too, for the same reason. Your options are a second account that does the approving, or a forced pull request plus Control 1's deny plus your own habits. The second is the weaker one.

Then the step people skip: keep your own account off the bypass list. Bypass is something you grant per ruleset. Your agent pushes with your credentials, so any rule you can bypass, it can bypass. A ruleset with you on the bypass list protects main from everyone except the one process most likely to push to it by accident.

The same goes for admin rights. If your account is an admin on the repo, so is your agent, and it can edit the ruleset itself through gh api. The ruleset stops an agent that pushes to main by mistake. It doesn't stop one using your admin login on purpose; for that, the agent needs its own account or a sandbox.

With approvals from someone else, this layer comes closest to what GitHub did for its cloud agent. The agent writes to its own branch, and main moves only after another human approves the pull request.

A branch per task, and the push is still yours to stop

I build Hermes IDE, so weigh this section with that in mind. Hermes runs Claude Code, Codex, GitHub Copilot CLI, OpenCode and other CLI agents side by side, each in a real terminal (README). ⌘N (Ctrl+Shift+N on Windows and Linux) starts every task on its own hermes/<name> branch, in its own worktree, with the task as the agent's first prompt. That's why hermes/ is already in the hook's prefix list: every task branch can leave the clone, and nothing outside agent/ and hermes/ can.

The Review Desk's Worktrees tab in Hermes IDE, listing one worktree each for orders-api, web-app and orders-types under ~/code, each on its own hermes/ branch with its task, age and disk size
The Worktrees tab in the Review Desk: one worktree per task, each on its own hermes/ branch.

Landing is something you click. Land commits, opens a pull request or merges locally in one step, into the branch the task started from, and it can be undone. Before that, the Review Desk (⌘G, Ctrl+Shift+G on Windows and Linux) shows the agent's changes by turn or by file, with risk flags. With the ruleset from Control 3 turned on, Land's pull request route is the one that fits: the task branch goes up, and main moves once the pull request is approved and merged.

The Hermes Review Desk in By turn view for a Claude Code task in orders-api, showing two turns, the first turn's diff of src/db.ts and a commit message drafted from the turns
The Review Desk by turn: two agent turns, the first turn's diff, and a commit message drafted from them.

What Hermes doesn't do is sandbox the agent. Each one runs its own interface in a real terminal with your own sign-ins, as it would outside Hermes. If an agent types git push origin main in that terminal, it gets whatever your credentials allow. Hermes never answers a permission prompt for you and never types into a terminal on its own, but the deny rule, the hook and the ruleset are what stop the push. Hermes is free, source-available under BSL 1.1, runs on macOS, Windows and Linux, and isn't affiliated with GitHub, Anthropic, OpenAI or Nous Research. Download it here.

Open the ruleset on your default branch and look at two things: the bypass list, and the required approvals count. Is your own account on the first one, and is the second one still zero? Tell me in the comments.


If this was useful

If you run agents with your own credentials, Hermes IDE gives each task its own hermes/ branch and makes landing a click you can undo (GitHub). The fences in this post are one answer to a bigger question: how much autonomy you hand an agent, and where you take it back. The AI Agents Pocket Guide is my short book on the patterns behind that, autonomy and guardrails included. It's one of 8 short books in the Pocket Guides for Developers series, on agents, RAG, prompts, observability, events, databases and system design.

AI Agents Pocket Guide: Patterns for Building Autonomous Systems with LLMs

Top comments (0)