Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ permissions allow it.
| Cursor | hooks installed; `pgx agent exec --` when `PGX_ISOLATION` is unset | env propagation pending live verification |
| Codex | `pgx agent exec -- codex` (Codex hooks cannot set environment) | wrapper verified |

Requires PGX 0.1.1 or newer ([release v0.1.1](https://github.com/pgrundev/pgx/releases/tag/v0.1.1)); the Skill checks the installed version before it does anything.
Details, the lifecycle, security rules and the validation notes:
[`docs/pgx/local-dev/agents.md`](docs/pgx/local-dev/agents.md). The Skill
itself: [`skills/pgx/SKILL.md`](skills/pgx/SKILL.md).
Expand Down
10 changes: 10 additions & 0 deletions docs/pgx/local-dev/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ session on, isolation is automatic on the hosts marked verified below. A
Skill installation on its own executes nothing; the agent performs setup when
the Skill is activated and the host's permissions allow it.

## Versions

`pgx agent` and the Skill need **PGX 0.1.1 or newer**
([release](https://github.com/pgrundev/pgx/releases/tag/v0.1.1): linux-amd64,
linux-arm64, darwin-arm64 with SHA-256 checksums). The Skill's
`scripts/check-pgx.sh` verifies the installed binary from `pgx --json version`
and refuses older ones (the published 0.1.0 has no `pgx agent`). npm
`pgxdb@0.1.1` follows the GitHub release through the repository's staged
publishing process.

## What you get

| | |
Expand Down
12 changes: 11 additions & 1 deletion docs/pgx/local-dev/validation/2026-10-25-agent-live-claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,19 @@ user in `agent-claude-11111111` (count 1); B saw count 0 and no such user in
`agent-claude-22222222`. Both reported their database name from Rails
itself.

## Runs after the review fixes (2026-10-26)

| Prompt | Turns | Cost | Result |
|---|---|---|---|
| Fail-closed: `.pgx.toml` names a missing template, PGX stopped, the developer's PostgreSQL `DATABASE_URL` inherited; "run the pending migrations now" | 5 | $0.75 | The hook wrote the sentinel (`DATABASE_URL=postgres://pgx-isolation-failed@127.0.0.1:1/…`, `PGX_ISOLATION=failed`) and exited 0 so Claude Code kept it; the agent read the reason, ran `pgx agent doctor`, and refused ("no database was touched"). The developer database's `schema_migrations` and columns were unchanged. |
| Re-validation on the release code: "add a unique index on users.email via a migration, run it and the tests" | 14 | $1.05 | Fresh session, branch `agent-claude-4f689f41`, migration and suite green (4 runs, 0 failures); the template kept one index, the branch had two; the dev PostgreSQL fingerprint unchanged. |
| **Public artifact**: Skill installed from public `main`, PGX from the published `v0.1.1` darwin-arm64 tarball (checksum verified), setup by the documented commands, then "Add a last_login_at timestamp to users and run the tests." | 7 | $0.8 | Branch `agent-claude-90c67043` created by the hook (`PGX_ISOLATION=ok`), migration and tests green (3 runs, 5 assertions), the branch has the column, template and dev PostgreSQL do not, fingerprint unchanged, branch retired after the session. 53 seconds end to end. |

Seven runs in total: $9.00 for the first six plus $0.8.

## Verdict

Automatic isolation on Claude Code is verified end to end: Skill discovery
Automatic isolation on Claude Code is verified end to end, including with the public 0.1.1 artifact: Skill discovery
and activation, PGX setup from an existing PostgreSQL, hook-driven branch
creation with environment injection, real migrations and tests, resume
within grace, and concurrent isolation, with the developer's database
Expand Down
15 changes: 11 additions & 4 deletions skills/pgx/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,17 @@ PGX database), `none` (no database yet), `remote` (another host),

### 2. Make sure PGX is available

PGX **0.1.1 or newer** is required: `pgx --json version` must report a
`pgx_version` of at least 0.1.1 (`pgx agent ls` exits 2 with "unknown
command" on anything older; note that `pgx agent --help` prints the global
help on every version and proves nothing). If `pgx` is missing or too old,
PGX **0.1.1 or newer** is required. Run the check script next to this file:

```sh
bash .claude/skills/pgx/scripts/check-pgx.sh # or .agents/skills/pgx/scripts/check-pgx.sh
```

It prints `{"ok":true,"version":…}` and exits 0 for a compatible binary;
exit 3 means `pgx` is missing, 2 too old (for example the published 0.1.0,
which has no `pgx agent`), 4 an unreadable `pgx --json version`. It reads the
structured version only: `pgx agent --help` prints the global help on every
version and proves nothing. If `pgx` is missing or too old,
follow [references/installation.md](references/installation.md): prefer an
installed binary, then `npm i -g pgxdb` (Node 18+), then the official
installer `curl -fsSL https://pgx.dev/install | sh`. Supported: macOS arm64,
Expand Down
15 changes: 9 additions & 6 deletions skills/pgx/references/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ pgx --json status # exit 3 when initialised but stopped;
```

The skill needs **PGX 0.1.1 or newer**, the first release with the
`pgx agent` command group. Check `pgx --json version` → `pgx_version`
(compare as semver: 0.1.1 ≤ 0.1.1 < 0.2.0), or run `pgx agent ls`, which
exits 2 with "unknown command" on an older binary. Do not use `pgx agent
--help` as the test: `--help` prints the global help on every version. An
older binary: upgrade with the method it was installed with (`npm i -g
pgxdb@latest`, or the installer again) and re-check the version.
`pgx agent` command group (https://github.com/pgrundev/pgx/releases/tag/v0.1.1).
`scripts/check-pgx.sh` decides: exit 0 compatible, 3 missing, 2 too old, 4
malformed, always with one JSON line. It compares `pgx --json version` →
`pgx_version` as semver (`PGX_MIN_VERSION` overrides the floor). Do not use
`pgx agent --help` as the test: `--help` prints the global help on every
version. An older binary: upgrade with the method it was installed with
(`npm i -g pgxdb@latest`, or the installer again) and run the check again;
if the upgrade still reports 0.1.0, the newer package is not yet on that
channel and the user should install from the GitHub release.

## Install, in order of preference

Expand Down
31 changes: 31 additions & 0 deletions skills/pgx/scripts/check-pgx.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# check-pgx.sh: is a compatible PGX on PATH? Prints one JSON object and exits
# 0 compatible (version >= MIN) {"ok":true,"version":"0.1.1","path":"…"}
# 3 pgx not found {"ok":false,"reason":"missing",…}
# 2 too old {"ok":false,"reason":"too_old","version":"0.1.0",…}
# 4 unreadable version output {"ok":false,"reason":"malformed",…}
# `pgx agent --help` exits 0 on every PGX version (global help), so only the
# structured `pgx --json version` output counts.
set -u
MIN=${PGX_MIN_VERSION:-0.1.1}
esc() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; }
path=$(command -v pgx 2>/dev/null || true)
if [ -z "$path" ]; then
printf '{"ok":false,"reason":"missing","min":"%s","hint":"install PGX: npm i -g pgxdb (Node 18+) or curl -fsSL https://pgx.dev/install | sh"}\n' "$MIN"; exit 3
fi
out=$(pgx --json version 2>/dev/null || true)
ver=$(printf '%s' "$out" | sed -nE 's/.*"pgx_version":"([0-9]+\.[0-9]+\.[0-9]+)[^"]*".*/\1/p' | head -1)
if [ -z "$ver" ]; then
printf '{"ok":false,"reason":"malformed","path":"%s","min":"%s","hint":"pgx --json version did not report pgx_version; reinstall PGX"}\n' "$(esc "$path")" "$MIN"; exit 4
fi
# semver compare on major.minor.patch
vcmp() { # returns 0 if $1 >= $2
local a b; IFS=. read -r a1 a2 a3 <<<"$1"; IFS=. read -r b1 b2 b3 <<<"$2"
[ "$a1" -gt "$b1" ] && return 0; [ "$a1" -lt "$b1" ] && return 1
[ "$a2" -gt "$b2" ] && return 0; [ "$a2" -lt "$b2" ] && return 1
[ "$a3" -ge "$b3" ]
}
if vcmp "$ver" "$MIN"; then
printf '{"ok":true,"version":"%s","path":"%s","min":"%s"}\n' "$ver" "$(esc "$path")" "$MIN"; exit 0
fi
printf '{"ok":false,"reason":"too_old","version":"%s","path":"%s","min":"%s","hint":"PGX %s has no pgx agent; upgrade with the method it was installed with (npm i -g pgxdb@latest, or the installer again)"}\n' "$ver" "$(esc "$path")" "$MIN" "$ver"; exit 2
20 changes: 20 additions & 0 deletions skills/tests/pgx/check-pgx.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Tests for scripts/check-pgx.sh: missing, too old, current, malformed.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd); chk="$here/../../pgx/scripts/check-pgx.sh"
fail=0; ok() { echo "ok $1"; }; bad() { echo "FAIL $1"; fail=1; }
mk() { # name version-json-or-garbage
d=$(mktemp -d); printf '#!/bin/sh\ncase "$*" in *version*) printf %%s %s ;; *agent*--help*) echo "pgx help"; exit 0 ;; esac\n' "'$2'" > "$d/pgx"; chmod +x "$d/pgx"; echo "$d"
}
run() { PATH="$1:/usr/bin:/bin" bash "$chk"; }
set +e
out=$(PATH=/usr/bin:/bin bash "$chk"); rc=$?; [ $rc = 3 ] && [[ $out == *'"reason":"missing"'* ]] && ok "missing → 3" || bad "missing: rc=$rc $out"
d=$(mk old '{"pgx_version":"0.1.0","os":"darwin"}'); out=$(run "$d"); rc=$?; [ $rc = 2 ] && [[ $out == *'"too_old"'* ]] && ok "0.1.0 → 2 (too old) even though agent --help exits 0" || bad "old: rc=$rc $out"
d=$(mk cur '{"pgx_version":"0.1.1","os":"darwin"}'); out=$(run "$d"); rc=$?; [ $rc = 0 ] && [[ $out == *'"ok":true'* ]] && ok "0.1.1 → 0" || bad "current: rc=$rc $out"
d=$(mk new '{"pgx_version":"0.2.0","os":"linux"}'); out=$(run "$d"); rc=$?; [ $rc = 0 ] && ok "0.2.0 → 0" || bad "newer: rc=$rc $out"
d=$(mk ci '{"pgx_version":"0.0.0-ci.abc","os":"linux"}'); out=$(run "$d"); rc=$?; [ $rc = 2 ] && ok "0.0.0-ci → 2" || bad "ci build: rc=$rc $out"
d=$(mk bad 'not json at all'); out=$(run "$d"); rc=$?; [ $rc = 4 ] && [[ $out == *'"malformed"'* ]] && ok "garbage → 4 (malformed)" || bad "malformed: rc=$rc $out"
d=$(mk empty ''); out=$(run "$d"); rc=$?; [ $rc = 4 ] && ok "empty → 4" || bad "empty: rc=$rc $out"
for o in "$(PATH=/usr/bin:/bin bash "$chk")" "$(run "$(mk x '{"pgx_version":"0.1.0"}')")"; do python3 -I -c 'import json,sys; json.loads(sys.argv[1])' "$o" && ok "output is JSON" || bad "not JSON: $o"; done
set -e
[ $fail = 0 ] && echo "CHECK-PGX TESTS PASSED" || { echo "CHECK-PGX TESTS FAILED"; exit 1; }
5 changes: 5 additions & 0 deletions skills/tests/pgx/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,9 @@ done
# The output is one JSON object per run, nothing else on stdout.
j rails | python3 -I -c 'import json,sys; json.load(sys.stdin)' && ok "detect output is a single JSON document" || bad "detect output is not clean JSON"

# 4. The version-check script.
[ -x "$skill/scripts/check-pgx.sh" ] && ok "scripts/check-pgx.sh executable" || bad "scripts/check-pgx.sh missing or not executable"
"$here/check-pgx.test.sh" >/tmp/check-pgx.out 2>&1 && ok "check-pgx.sh: missing/old/current/malformed" || bad "check-pgx.sh tests: $(tail -3 /tmp/check-pgx.out)"
grep -q 'check-pgx.sh' "$skill/SKILL.md" && ok "SKILL.md points at check-pgx.sh" || bad "SKILL.md must use check-pgx.sh"

[ "$fail" -eq 0 ] && echo "ALL SKILL TESTS PASSED" || { echo "SKILL TESTS FAILED"; exit 1; }
Loading