Install
$ agentstack add skill-jdpalumbo2-claude-skills-clodex-ship Open-source listing, not yet scanned by AgentStack. Follow the source repository for install instructions.
Security review
⚠ Flagged1 finding(s); flagged for manual review. · v0.1.0 How review works →
- • Prompt-injection patterns
- • Secret / credential exfiltration
- • Dangerous shell & filesystem operations
- • Untrusted network calls
- • Known-malicious package signatures
- high Dangerous shell/eval execution.
What it can access
- ● Network access Used
- ● Filesystem access Used
- ● Shell / process execution Used
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
clodex-ship — one authorization, then two-phase steps that reconcile before they retry
Overview
This is the only stage in clodex that changes anything outside the repository, so it is the only one whose mistakes cannot be undone by editing a file. It owes the run three things, all of them fact in the event log:
| Fact | Event | In the run manifest | |---|---|---| | exactly what the user authorized — actions and accepted debt | approval:granted | approvals[] with scope: "release-authorization" | | every external step, before and after it happened | release:step:pending → release:step:done / :failed | release.steps[] | | where the release ended up | release:updated | release.state, .tag, .deployed, .verified_live |
This is the single binding debt gate in clodex
clodex-plan declared the evidence classes. clodex-verify produced what it could and recorded the rest as verification debt — deliberately without gating on it, because there is exactly one place in this system where a human accepts debt, and it is here, inside the release authorization (§5), in the same message as the exact commands the release will run.
So: non-empty debt is itemised in the authorization, class by class, with its reason and its risk, and it is accepted in words. Never inferred from silence, never rolled into "approve?", never assumed because the user already saw it at verify. They did not accept it at verify. Nobody has.
Where this stage ends
In the repository tree you write two files and no others: the profile's changelog.path and its version.source (§2). Working artifacts — a diff, a prompt, a log — go in the run directory, which is gitignored. You do not edit code, tests, or docs: those were clodex-build's under a batch contract, and a change to one here is a finding, not a quiet edit. You cannot send the run backwards: the reducer refuses it.
You arrive here from clodex or from clodex-verify, which hands you an absolute run directory. If you were invoked without one, stop and invoke clodex — do not go looking for a run yourself.
0. Paths and commands
CLODEX_HOME="${CLODEX_HOME:-$HOME/.claude/skills/clodex}" # the router's dir, not this one
STATE="$CLODEX_HOME/state/clodex_state.py"
RUNNER="$CLODEX_HOME/runner/run-codex.sh"
RUN_DIR=""
SNAP="$(python3 "$STATE" rebuild "$RUN_DIR")"
REPO="$(printf '%s' "$SNAP" | python3 -c 'import json,sys;print(json.load(sys.stdin)["repo"])')"
PLAN="$(printf '%s' "$SNAP" | python3 -c 'import json,sys;print(json.load(sys.stdin)["plan"]["path"] or "")')"
RUN="$(printf '%s' "$SNAP" | python3 -c 'import json,sys;print(json.load(sys.stdin)["run"])')" # the run id — every op_id starts with it
cd "$REPO"
PROFILE="$REPO/.clodex/profile.json"
RUNNER_STATE="${CLODEX_RUNNER_STATE_DIR:-$REPO/.clodex/runner}"
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
REMOTE="$(git rev-parse --abbrev-ref '@{upstream}' 2>/dev/null | cut -d/ -f1 || true)"
[ -n "$REMOTE" ] || REMOTE="$(git remote | head -1)" # empty = this repo has no remote
Shell variables do not survive between command invocations — re-establish this block at the top of every shell you run these procedures in. Everything below runs from $REPO: git, the profile reads, and every action's relative cwd resolve against the current directory, so from a subdirectory they answer the wrong question.
$BRANCH comes from git, not from the manifest. snapshot.branch records where the run opened; clodex-build §4 may have created a run branch afterwards and no event records that. What you push is the branch you are on.
Engine verbs, payload on stdin:
python3 "$STATE" status "$RUN_DIR" # human summary; its `release:` line names any pending step
python3 "$STATE" rebuild "$RUN_DIR" # the manifest: full snapshot JSON
python3 "$STATE" append "$RUN_DIR" ` — this run is at stage ``, not ship."* You
**cannot** pull it forward yourself; the stage before you appends its own entry
event, and skipping one would leave the log claiming a stage that never ran.
- `closed` → the run is over. Do not reopen it, do not append to it. If the user
wants more, `clodex` opens a new run carrying this one as its `parent`.
**Resume map** — read the manifest and match the first row that is true. The
first row exists because it is the only one that can cost real money:
```bash
python3 "$STATE" rebuild "$RUN_DIR" | python3 -c '
import json,sys
snap = json.load(sys.stdin); rel = snap["release"]
print("release state:", rel["state"], "| tag:", rel["tag"], "| deployed:", rel["deployed"])
print("timestamp:", rel["timestamp"], "| verified_live:", rel["verified_live"])
for s in rel["steps"]:
print(" step %-12s %-8s op_id=%s reconciled=%s" % (s["step"], s["status"], s["op_id"], s["reconciled"]))
for a in snap["approvals"]:
print(" approval", a["scope"], "REVOKED" if a["revoked"] else "standing",
"| actions:", [x.get("id") for x in a["actions"]],
"| accepted_debt:", [x.get("class") for x in a["accepted_debt"]])
print("debt recorded:", [d.get("class") for d in snap["verification"]["debt"]])'
| What you find | You are | Go to | |---|---|---| | a step with status: "pending" | interrupted while an external action was in flight | §8, first, before anything else. Reconcile it against reality. Do not retry it, do not run the next step, do not re-authorize. | | release.state is push-failed or deploy-failed, nothing pending | a step failed and was recorded | §8's decision table | | no release-authorization approval, or the only one shows REVOKED | before the gate | §3 | | a standing release-authorization and no steps | authorized, nothing run | §7.1 — the first step. §6 is the mechanism each step runs through, not a place in the sequence. | | steps done through some point in §7's order | mid-sequence | the next step in §7's order | | release.state is verified-live, not-deployed, or abandoned | terminal | §10 — close the run if stage is not already closed |
The reducer does not de-duplicate. Read the state above before appending anything: a session that died after acting appended nothing about it, and a session that died after appending left the fact in the log where you can see it.
2. What this stage owns, and what it must never touch
The changelog and the version source, and nothing else. The bookkeeping step writes the profile's changelog.path and its version.source, and the release commit stages exactly those paths. That is the whole of what ship writes.
Usually two files. changelog.path may also be the directory releases are written into (profile.schema.json says so, and changelog.style is where a repo records a convention like one file per release plus a rollup row). Then the release writes more than one changelog file, and their names come from the convention rather than from the profile — so the authorization names them: changelog becomes an object mapping each path to its exact text, and the bookkeeping descriptor's writes is that list plus the version source (§5). Everything downstream reads that one list.
python3 - "$PROFILE" /dev/null`. Reconciliation is
built out of them, so treating a read as an action would make §8 impossible.
---
## 3. The final review — against the hash that was approved
Three questions, and the first two are mechanical.
**(a) Is the approved plan still the plan?** Approvals bind to a plan content
hash. An amendment supersedes that hash and revokes every approval bound to it,
and each amendment declares the re-review it requires. **A declared re-review
that has not happened blocks ship** — that is this stage's job, not build's, and
you check it again here because build's check ran before verify:
```bash
python3 - "$STATE" "$RUN_DIR" "$RUNNER_STATE" / (no run id) and // (runner --run-id).
for path in sorted(glob.glob(os.path.join(runner_state, "*", "*.envelope.json"))
+ glob.glob(os.path.join(runner_state, "*", "*", "*.envelope.json"))):
try: # a half-written envelope is an absent review,
env = json.load(open(path)) # not a reason to crash the gate
except (OSError, ValueError):
print("unreadable envelope, treated as absent:", path)
continue
if env.get("status") == "complete" and any(
i.get("sha256") == disk for i in env.get("inputs", []) if isinstance(i, dict)):
ran.add(env.get("role"))
declared = sorted({r for a in plan["amendments"] for r in a["required_review"]})
print("declared re-reviews:", declared or "(none)", "| evidenced:",
sorted(ran) or ("(none)" if disk is not None else "(not tested — no plan hash to match)"))
for role in declared:
if role not in ran:
blockers.append("amendment re-review %r has no complete envelope against the current plan" % role)
open_f = [f["id"] for f in snap["findings"] if f["disposition"] == "open"]
print("open findings:", " ".join(open_f) or "(none)")
if open_f:
blockers.append("findings still open: " + " ".join(open_f))
for b in blockers:
print("BLOCKER:", b)
print("REVIEW GATE OPEN" if not blockers else "BLOCKED — %d blocker(s)" % len(blockers))
PY
BLOCKED is not something you narrate past. A missing re-review is satisfied by running that role against the current plan — clodex-plan §8 for plan-reviewer, (c) below for code-reviewer — and nothing else clears it. That second one is deliberate, not a loophole: (c) reviews the whole release diff against the current plan, which is a superset of the batches any amendment affected, and it is why this script is run again at the end of §3. First pass it may say BLOCKED on a code-reviewer re-review that has not happened yet; after (c) runs, the same script sees the envelope. A plan file edited without an amendment is clodex-build §11, and this run cannot re-enter build, so it goes to the user with those two facts.
(b) Does every commit in this release belong to a batch? clodex-verify §8 gives the user a path where they fix something themselves and commit it. That commit is real, it is in the release, and no batch owns it. Find it:
python3 - "$STATE" "$RUN_DIR" "}
accepted when they vouch for it — the clodex-verify §8 user-fix path, whose own finding note carries their sha, which is why the script prints that hint. rejected when it should not be in this release, which stops the run: removing a commit from a branch is theirs to do, not yours. The rule underneath: a release never contains a change nobody named. Rewriting history to remove one is not a step this skill has.
(c) One Codex code-reviewer round over the whole release diff. Build reviewed each batch against its own contract; this looks at all of it at once against the plan. Default-on, with exactly one skip, and it is a predicate over git diff --name-only with nothing left to interpret: skip only when every path in the release diff ends .md or is the plan file. Everything else — a test, a fixture, a workflow, a Dockerfile, a lockfile, a Makefile — gets the round. Drawing the line at "does this file run?" sounds tighter and is not: it turns a git diff --name-only into an argument about tests/helpers/, and the round is cheap next to the release. Say in chat which of the two you did, and list the paths you decided on.
START="$(python3 "$STATE" rebuild "$RUN_DIR" | python3 -c 'import json,sys;print(json.load(sys.stdin)["git"]["start_head"])')"
git diff "$START" HEAD > "$RUN_DIR/release.diff"
git diff --name-only "$START" HEAD # read this before deciding the skip
PROMPT="$RUN_DIR/release-review.prompt.md" # written with your file tool, never a shell string
RC=0
OUT="$(bash "$RUNNER" --role code-reviewer --repo "$REPO" \
--run-id "$(basename "$RUN_DIR")" \
--prompt-file "$PROMPT" --input "$RUN_DIR/release.diff" --input "$PLAN")" || RC=$?
printf 'rc=%s line=%s\n' "$RC" "$OUT"
ENVELOPE="${OUT#* }" # strip the FIRST word only — a repo path may contain spaces
Read the envelope rather than the runner's prose — it is what the rc table below sends you to, and it proves which diff was reviewed:
python3 - "$ENVELOPE" "$RUN_DIR/release.diff" — read its Scope ("Done when"), Batches, and Evidence.
Diff under review: — every commit this run made.
Report as findings:
1. Something the plan's "Done when" requires that this diff does not deliver.
2. Two batches that disagree — a caller and a callee, a writer and a reader, a
config and the code that reads it.
3. A change outside what the plan describes.
4. A credential, token, or key VALUE in the diff. Names are fine; values never are.
Report findings only. Do not edit files and do not propose patches: this run's
build stage is over. blocker/high/medium for anything that should not be
released; low/info for improvements. Return an empty findings list if you find
nothing.
The runner prints one line, " ", and its exit code is the authority — never read status out of prose or stderr:
| rc | Status | What to do | |---|---|---| | 0 | complete | Read the findings. | | 2 | partial | It stopped short. Resume with the one-command line the runner printed on stderr — do not start a fresh invocation. | | 3 | interrupted | Same: resume with the printed command. | | 1 | failed | Read the envelope's error and output.stderr. A failed worker is an absent review, not a clean bill: run it once more, and if it fails again, add a line to §5's message — *"the final code review did not run: `"* — so the user authorizes with that known. | | 64 | usage error | You called the runner wrong. Fix the arguments. | | — | empty $OUT` | The runner died before writing an envelope. Read its stderr. |
(The runner's rc 3 is interrupted — resume it. That is a different 3 from the state engine's "logged, do not retry" in §0; the two tools each own their codes, and mixing them up costs a duplicate event or a lost review.)
Record every finding with source: "code-reviewer", continuing the same s- sequence §3(b) started — if it recorded s-F001, these are s-F002 onward, and finding:recorded refuses a duplicate id. Each carries invocation, and the first of them carries the codex block (clodex → Telemetry) with this round's role, status, envelope, input_hashes and duration_s. A round that finds nothing carries it too — put it on the approval:granted event §5 appends, because a release whose final review found nothing and a release whose final review never ran must not look the same in the manifest. This is the only Codex round this stage makes, and it is the one the release rests on.
Dispose every one before §5. What carries over unchanged from clodex-plan §9: the three dispositions, that only the user may accept or reject one and their words go in the note, that nothing is ever dropped, and that an accepted blocker is a legitimate end state. What is different here: fixed means code changed, and this stage cannot change code. A finding that needs a code fix has the same three outcomes clodex-verify §8 lists — a follow-on run, the user fixing and committing it themselves (which then appears as an unowned commit in (b), and is named there), or the user accepting it.
Then run (a)'s script again, last, once every finding from (b) and (c) is disposed. It is the gate, and (b) and (c) both add findings to what it checks; REVIEW GATE OPEN on that second run is what lets you write §5's message.
4. Assemble the release: what this repo's profile says will happen
The step list is computed from the profile, not chosen. Print it:
python3 - "$PROFILE" "$BRANCH" "$REMOTE" ", a.get("target") or "-",
", ".join(a.get("env_refs") or []) or
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [jdpalumbo2](https://github.com/jdpalumbo2)
- **Source:** [jdpalumbo2/claude-skills](https://github.com/jdpalumbo2/claude-skills)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.