Skip to content
Agentmart
Submit a tool

push-ci

Push to remote and monitor CI.

Skill by sd0xdev

IndexedMIT★ 194
Write a review

Chalk Labs

Need help deploying push-ci?

Chalk Labs sets up agents like this inside real teams: connected to your tools, tested on your data, with a human in the loop where it matters.

Book a call

Install

Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.

npx agentmarket install skill/sd0xdev-push-ci

Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.

Get the weekly diff

The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.

We use your email only for this newsletter. Privacy

About

Push to remote and monitor CI. Validates branch safety, executes git push WITH explicit user approval, then monitors CI run status via gh CLI. Use when: user says 'push', 'push and watch CI', 'ship it', 'push-ci'. Not for: committing (use /smart-commit), creating PRs (use /create-pr), merging (use /merge-prep).

Use cases: devops

No reviews yet. Installed it? Reviews come from verified installers only. How reviews work

Permissions

What this skill can do and what it asks for, from its SKILL.md. Checked Oct 25, 2026.

Tools
No tools found
Secrets
None asked for
Install scripts
None found

Network hosts it talks to 1

  • codeload.github.com · found in install command

Files it touches

  • ~/.claude/skills/push-ci

Tools it pre-approves 8

From allowed-tools in SKILL.md: the agent may use these without asking you each time.

  • Bash(git:*),
  • Bash(gh:*),
  • Bash(bash:*),
  • Bash(/bin/bash:*),
  • Read,
  • Grep,
  • Glob,
  • AskUserQuestion

What's inside

Skill name
push-ci
Description
Push to remote and monitor CI. Validates branch safety, executes git push WITH explicit user approval, then monitors CI run status via gh CLI. Use when: user says 'push', 'push and watch CI', 'ship it', 'push-ci'. Not for: committing (use /smart-commit), creating PRs (use /create-pr), merging (use /merge-prep).
License
MIT
Allowed tools
Bash(git:*),, Bash(gh:*),, Bash(bash:*),, Bash(/bin/bash:*),, Read,, Grep,, Glob,, AskUserQuestion
Instructions
1,762 lines in SKILL.md · view on GitHub

Before installing

  • Pre-approves some shell commands
  • Instructions read files in your home folder (~/.ssh, ~/.aws…)

Agent Skills spec issues

  • body: 1762 lines (spec recommends under 500)
  • files: referenced but missing: scripts/pre-push-gate.sh

Files (2)

  • SKILL.md
  • authorization-contract.md · references/

SKILL.md (first lines)

# Push & CI Monitor

**Read first**: `references/authorization-contract.md` before any push — § Push safety for the credential and the two prompt classes, § Efficacy Boundary for what an approval here does and does not authorize. If that Read fails, stop and report it; do not push.


Push to remote with user approval, then monitor CI run until completion.

## Authorization

```
⚠️ This skill is one of three *skill* paths authorized to execute `git push`, beside Register #4's
⚠️ `user-authorized execution` — the user's own message naming the push, which no skill can grant,
⚠️ request or infer, and which this block must never be cited to refuse.
⚠️ The second is /epic-merge (--force-with-lease for stacked PR chains, per-iteration AskUserQuestion gate).
⚠️ The third is /gh-stack, which pushes through `gh stack link|push|submit --auto` — a plain `--atomic` push for link, a per-branch `--force-with-lease` for the other two (per-use AskUserQuestion gate). It never pushes a branch directly.
⚠️ This skill may also use --force-with-lease, but only when the caller passes the flag — and NEVER onto a protected branch; bare --force is forbidden everywhere.
⚠️ /deploy-flow never runs git push itself; a declared `run` step under `Run Steps: execute` may push on its own, outside this skill — the project's opted-in, stated risk.
⚠️ All other skills and rules MUST output push commands only (not execute).
⚠️ Push REQUIRES explicit user approval via AskUserQuestion — no exceptions.
```

| Rule | This Skill | `/epic-merge` | `/gh-stack` | `/deploy-flow` | All Other Skills |
|------|-----------|---------------|-------------|----------------|------------------|
| `git push` | Execute (after user approval) | Forbidden (uses `--force-with-lease` only) | Never directly — only as what `gh stack link` runs underneath (`--atomic`, no force), after a per-use AskUserQuestion naming that form | Never itself; a `run` step under `Run Steps: execute` may push outside this skill (run-script risk) | Forbidden (output only) |
| `git push --force` | Forbidden | Forbidden | Forbidden | Forbidden | Forbidden |
| `git push --force-with-lease` | Execute — **only** when `--force-with-lease` is explicitly passed, after user approval naming the force form; **never onto a protected branch** (Phase 0 hard-aborts, Phase 2 re-asserts) | Execute (after per-iteration AskUserQuestion) | Execute **through the extension** (`gh stack push` / `submit --auto`, per-branch value-bearing lease), after the unshared attestation and a per-use AskUserQuestion naming the force form; a protected branch may be the stack's base, never a layer | Forbidden | Forbidden |
| Push to protected branches (main/master/develop/release/*) | Warn + pre-approval via AskUserQuestion (final gate is the terminal hook when installed, otherwise this approval); with `--force-with-lease` → **hard abort**, no question asked | Protected PR heads rejected — Phase 0 validation, re-asserted before Step 5 and Rollback (a PR head is not inherently unprotected) | Refused as a stack layer in Phase 2; the chain's base may be one | Never itself — a declared merge into one is local, and a later push goes through this skill | Forbidden |

## Defense in Depth: Push Safety

| Layer | Mechanism | Scope | Reliability |
|-------|-----------|-------|-------------|
| **L1: git pre-push hook** (opt-in) | `pre-push-gate.sh` reads `/dev/tty` for terminal confirmation | **Two classes reach the prompt** (since 2026-08-21): a **protected branch** with `ALLOW_PUSH_PROTECTED` unset, and a **history-rewriting push** whose rewritten refs are not already covered by that first prompt, with `ALLOW_FORCE_UNSHARED` unset. Non-fast-forward is an earlier, orthogonal refusal — this hook's own `exit 1` only when git hands it the ref (the force form); otherwise git refuses first and this hook never sees it (§ below). Skipped by `ALLOW_FORCE_WITH_LEASE=1`, after which the push falls through to the **unshared attestation first** and the protected check second — not straight to the protected check (`scripts/pre-push-gate.sh`: the refusal at the force-form check, then the rewrite gate, then the protected gate) | Immune to Claude Code permission caching — **when installed**, and only for the classes it prompts on |
| **L2: AskUserQuestion** | In-session prompt before push | All pushes | May be auto-approved by session caching |
| **L3: git-workflow rules** | Claude forbidden from raw `git push` | All contexts | Behavioral enforcement |

**Which layer authorizes depends on whether L1 is installed.** The `pre-push` hook is opt-in (`/codex-setup init --with-push-gate`, or `sync --with-push-gate` on an existing project). `/install-scripts` copies `pre-push-gate.sh` into `.claude/scripts/` and never wires up a hook, so having run it is not evidence the gate exists.

**L1 prompts for two classes of push, and they are different questions.** The **protected** prompt asks *may this branch be pushed to at all*; it fires when the ref set includes a protected branch and `ALLOW_PUSH_PROTECTED` is unset. The **unshared attestation** asks *is anybody else working on the refs this push rewrites*; it fires when the push **rewrites a ref** and `ALLOW_FORCE_UNSHARED` is unset, over the rewritten refs the protected prompt will not already cover. **Rewrite is read per ref class, because ancestry is the *branch* rule**: a branch update is a rewrite when the remote tip is not provably an ancestor of what replaces it, while **every update to an existing tag is one** — git requires force semantics for any change to an existing `refs/tags/*` ref, forward moves included, because a tag names one commit rather than a line of history (`scripts/pre-push-gate.sh` § `is_tag_ref`, and the hook prints a separate line for tags precisely because a forced tag update can be a textbook fast-forward). A tag *creation* has no history to overwrite and is not asked about, and neither is a **deletion** of any ref class — the gate's rewrite test needs a non-null OID on both sides, so removing an existing tag or branch never reaches the **unshared attestation** (`rules/git-workflow.md` § Push safety states the boundary; the maintainer decided on 2026-08-22 to leave it there). That exclusion is from the rewrite class only: the gate collects every destination branch into its protected-branch check *before* the rewrite test runs, so deleting a **protected** branch — `main`, say — still reaches the protected prompt exactly as pushing to it would (`scripts/pre-push-gate.sh`, the `BRANCHES_PUSHING` collection and the protected gate). Only an unprotected deletion — or any deletion under `ALLOW_PUSH_PROTECTED=1`, which silences the protected prompt — reaches no prompt at all. Reading the class as "non-fast-forward" alone would wave through exactly the moves git itself classifies as forced — which is every rewritten ref when `ALLOW_PUSH_PROTECTED=1` has silenced that prompt. Neither variable clears the other. Every push the hook **permits** without meeting either condition exits 0 (`scripts/pre-push-gate.sh`). A push the hook *refuses* is a third outcome and not a row below: it never happens, so nothing authorized it. So the gate is chosen by **both** axes, and an installed hook never demotes L2 for a push the hook does not prompt on:

| Push (one the hook permits) | L1 installed | L1 not installed |
|------|--------------|------------------|
| Protected branch, `ALLOW_PUSH_PROTECTED` unset | **L1 authorizes** — `/dev/tty` confirmation, immune to permission caching. L2 stays required but advisory | **L2 authorizes** — the AskUserQuestion in Phase 0/1 |
| History-rewriting push — a **branch** update that is **not provably a fast-forward**, read fail-closed (the gate negates `merge-base --is-ancestor`, so an ancestry test that *errors* lands here exactly as one that answers no), **or any update to an existing tag** (forward moves included) — with `ALLOW_FORCE_UNSHARED` unset | **L1 authorizes** — the `/dev/tty` unshared attestation. L2 stays required but advisory | **L2 authorizes** — and Phase 1 must put the unshared question to the user itself (below), because without the hook nothing else will |
| Every other permitted push, an ordinary fast-forward included | **L2 authorizes** — L1 exits without prompting, so there is no terminal confirmation to defer to | **L2 authorizes** |

**The tag half of that second row is a hook-level fact, not a `/push-ci` one.** This skill only ever
builds a branch refspec, so a tag update cannot arise through it. The row states it anyway because
this table describes `pre-push-gate.sh` for whoever invokes it — a developer pushing a tag by hand
meets the same gate — and because a class stated as "non-fast-forward" would be read as a complete
definition by exactly the reader who then pushes a tag from the shell.

**Non-fast-forward is an orthogonal refusal, not a third row.** It is decided *before* the protected check and never produces a confirmation of its own: without `ALLOW_FORCE_WITH_LEASE=1` the push is refused outright (`exit 1`) — by the hook when it is given the ref, by git before that when no force flag was passed, and a refusal either way is not an authorization: nothing was approved, the push simply did not happen. With the variable set the hook's refusal is skipped entirely and the push falls through to the **same** protected-branch decision as any other. So it lands in whichever row its branch puts it in. Measured, all five shapes — **these are hook-level facts, describing `pre-push-gate.sh` for whoever invokes it**:

| Push | `ALLOW_FORCE_WITH_LEASE` | Result | Reachable via `/push-ci`? |
|------|--------------------------|--------|---------------------------|
| Unprotected, non-fast-forward, **no force flag** | unset or empty | **git** rejects the ref client-side; the hook runs with an **empty ref list** and exits 0 having never seen the branch | **Yes** — a flagless `/push-ci` on a diverged branch. The gate refused nothing here |
| Unprotected, non-fast-forward, `--force-with-lease` | unset | `exit 1` — the hook's own refusal, before the protected check, no prompt | **No** — Phase 2 sets the variable in the same branch that passes the flag. Only a manual force push reaches this cell |
| Unprotected, non-fast-forward, `--force-with-lease` | `1` | Reaches `/dev/tty` for the **unshared attestation** — the force row above, L1 authorizes. `exit 0` only once attested (or with `ALLOW_FORCE_UNSHARED=1`); no terminal ⇒ `exit 1` | **Yes** — this is the one force path this skill has |
| **Protected**, non-fast-forward, `--force-with-lease` | `1` | Reaches `/dev/tty` once — the protected prompt; the attestation excludes refs that prompt covers | **No** — Phase 0 hard-aborts protected × `--force-with-lease` before any question, and Phase 2 re-asserts it. Only a manual `git push --force-with-lease` reaches this cell |
| **Protected**, non-fast-forward, `--force-with-lease`, **`ALLOW_PUSH_PROTECTED=1`** | `1` | Reaches `/dev/tty` for the **unshared attestation** — the protected prompt is silenced, so the rewritten ref is no longer excluded from it | **No** — this skill never sets `ALLOW_PUSH_PROTECTED`. Only a manual push reaches this cell, and it is the combination that used to pass in total silence |

**Row 1 exists because git does not hand the hook a ref it has already rejected.** Measured on git 2.55.0 with a hook that dumps its stdin: a flagless push of a diverged branch runs the hook with **zero** ref lines — so `pre-push-gate.sh` finds no branches, detects no non-fast-forward, prompts on nothing and exits 0 — and the `! [rejected] … (non-fast-forward)` that stops the push is git's. Add `--force-with-lease` to the same push and the hook receives the ref line and its own refusal fires. Two consequences: the hook's non-fast-forward `exit 1` is reachable **only** on a push carrying a force flag, and a flagless `/push-ci` that fails on a diverged branch was stopped by git, not by the gate. Reading row 1 as a gate refusal would credit a credential to an operation no gate ever saw — the same error in the same direction as reading an absent hook as "no approval needed".

**The last column is the part a reader of this skill needs, and it is why the table is not just the hook's.** Three of the five shapes cannot happen through `/push-ci` at all: describing them as this skill's paths would promise a terminal confirmation on a route the skill refuses to take. The hook-level facts stay documented because the developer who pushes by hand still meets them.

The trap in both directions: grouping non-fast-forward *with* protected pushes claims a terminal credential for a push that is merely refused, and grouping it *against* them denies one for the third shape, where the terminal prompt genuinely happens — for a manual caller.

Reading "the hook is installed" as "something stronger will always ask" is the error this table exists to prevent — and **the table cannot supply its own counter-example**, because every cell in it either refuses the push or reaches `/dev/tty`. Rows 1 and 2 are refusals: the push does not happen, so nothing authorized anything. Rows 3–5 all prompt, so with the hook installed L1 is the credential there. The pushes whose **only** credential is the in-session approval are precisely the ones this table does not contain: every permitted push in neither prompting class — the row above it — and every push at all when the hook is not installed. Generalizing "installed ⇒ prompted" from these five rows is getting the wrong rule from the one table where it happens to hold.

**And the "L1 installed" column is a state of the world, not something Phase 0 can establish.** What Phase 0 reads is whether an executable hook *references* the gate — `PUSH_GATE=referenced`, never `installed` — and reference cannot prove invocation: a script that merely names the gate in a live command satisfies the same test. So the demotion in the top-left cell is earned by the operator **seeing the `/dev/tty` prompt**, never by the detection predicting one: if the approval is given and no prompt appears, that in-session approval was the only approval, whatever `PUSH_GATE` reported. This is why the approval is unconditionally required rather than skipped when L1 looks present — the check is allowed to be wrong in the unsafe direction precisely because nothing is skipped on its word.

**Why AskUserQuestion is the weaker of the two**: session permission caching can auto-approve AskUserQuestion calls in long-running sessions, especially with `-c` continue mode (GitHub Issue #15400). That weakness is the reason L1 is worth installing — it is **not** a licence to push without approval when L1 is absent. An uninstalled gate lowers the strength of the authorization; it never removes the requirement for one.

## Workflow

```mermaid
sequenceDiagram
    participant C as Claude
    participant U as User
    participant GH as GitHub

    C->>C: Phase 0: Preflight
    alt Protected branch + --force-with-lease
        C->>U: ⛔ Hard abort — prohibited combination, no question asked, no push
    else Permitted combination
        opt Protected branch (plain push)
            C->>U: ⚠️ Warning + ask pre-approval
            U->>C: Continue / Abort
        end
        C->>U: Phase 1: Show push plan + ask approval
        U->>C: Approve / Reject
        alt Approved
            C->>C: Phase 2: Protected×lease guard + command assembly
            C->>GH: Execute git push
            C->>GH: Phase 3: Delegate to /watch-ci
            GH-->>C: CI verdict
        else Rejected
            C->>U: Abort (no push)
        end
    end
```

### Phase 0: Preflight

Run all checks. Hard-abort on infrastructure failures; warn-and-confirm on protected branches — except protected branch × `--force-with-lease`, which hard-aborts (prohibited combination, see step 0 below).

```bash
# 0. Bind the two flag variables from THIS invocation, before anything reads them.
# Write the literal value on each line: `true` only when that flag appeared in the
# /push-ci invocation, `false` otherwise. Both lines are always written, always in
# this order, and never guarded by `:=`, `-z` or `[ -n … ]` — a guard would let an
# inherited environment value survive, which is the entire defect this step closes.
# Measured: with these two unbound and an ambient `FORCE_WITH_LEASE=true` exported,
# the Phase 2 assembly below runs `git push --force-with-lease=refs/heads/<b>:<tip>`
# for an invocation that passed no flag and a plan that showed a plain push. The
# approval must name the force form (`rules/git-workflow.md` § Exception), so the
# form has to come from the arguments and from nothing else.
FORCE_WITH_LEASE=false   # `true` only if the invocation contained --force-with-lease

From sd0xdev/sd0x-harness, MIT.

Signals

Stars
194
Forks
26
Open issues
5
Last commit (this folder)
Oct 3, 2026 (29 days ago)
Last release
Oct 6, 2026 (26 days ago)

From GitHub (fetched Oct 24, 2026).

Details

License
MIT
First seen
Oct 24, 2026
Last checked
Oct 24, 2026

More from sd0xdev

Done for you

Rather not set it up yourself?

Chalk Labs scopes, builds and runs agents for your team: the task audit, the eval harness, the integrations and the weekly ops report. Fixed scope, priced before we start.

See how we build AI agents →Read the case studies

Chalk Labs is a two-founder AI and growth studio. We build AI agents, CRMs and MVPs for Web3 and AI companies, and have shipped 50+ product and token launches.

launches shipped
50+
launches shipped
media placements
500+
media placements
founders on every account
2
founders on every account

Press we've placed: Forbes · CoinDesk · Decrypt · Cointelegraph · The Block

Rahil Jain, Co-founder, Chalk Labs. Ex-Goldman Sachs engineer. You talk to the person who builds it.

Get push-ci running in your business

Tell us what you want to automate. A founder replies within one working day with a plan, not a pitch deck.

Goes to the Chalk Labs founders, not the newsletter. Privacy

Need help deploying push-ci?

CallGet help
Cite this page

push-ci skill. Agent Mart. https://agentmart.dev/skills/sd0xdev-push-ci (last updated 2026-10-24)