Skip to content

Commit 47e237c

Browse files
committed
Fix permission and handoff push parity
1 parent 0dd1bd3 commit 47e237c

9 files changed

Lines changed: 201 additions & 35 deletions

File tree

.agents/skills/handoff/SKILL.md

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,29 @@ pair for `$resume-from-handoff`.
3131
claim.
3232
- If a command fails, halt the current phase and report the exact failure.
3333

34+
## Git Metadata Capability Gate
35+
36+
Before Phase 0, resolve the repository's actual Git metadata directory:
37+
38+
```bash
39+
git rev-parse --absolute-git-dir
40+
```
41+
42+
Run `test -w '<absolute-git-dir>'` against the exact returned path, shell-quoted.
43+
Do not request escalation for this check. The `game_studios` permission profile
44+
must make Git metadata writable. The same profile preserves normal workspace
45+
writes to `.agents/` and `.codex/` so CCGS's own maintenance workflows remain
46+
usable; only the declared secret paths stay denied.
47+
48+
If the lookup fails or the check reports the Git directory is not writable,
49+
halt before Phase 0. Report the exact path and result as a permission-profile
50+
configuration mismatch. Direct the user to install the current CCGS permission
51+
profile and start a new session so that profile becomes active. Do not begin the
52+
review gate, rotate continuity files, run `chmod`, delete lock files, or try an
53+
indirect command workaround. Do not tell the user to switch `/permissions`
54+
modes: approval routing cannot repair a filesystem rule already resolved for the
55+
session.
56+
3457
## Phase 0: Review Gate
3558

3659
Before rotating continuity files or committing, run this mandatory two-round
@@ -215,14 +238,21 @@ If there are no relevant uncommitted changes, skip the commit and say why.
215238

216239
Otherwise stage only the relevant paths by name. Avoid broad staging unless the
217240
user explicitly asked for it. `$handoff` invocation is commit authorization for
218-
the relevant handoff work. Before committing, verify:
241+
the relevant handoff work. Run staging without requesting escalation; the
242+
active `game_studios` profile grants Git metadata writes. If staging cannot
243+
write the Git index, halt Phase 3 and report the exact failure without retrying
244+
through a different command shape.
245+
246+
Before committing, verify:
219247

220248
```bash
221249
git diff --cached --name-status
222250
```
223251

224-
Commit with the standard handoff subject. Include a Codex co-author trailer only
225-
if that is normal for this repo.
252+
Commit with the standard handoff subject without requesting escalation. Include
253+
a Codex co-author trailer only if that is normal for this repo. If the commit
254+
cannot write Git metadata, halt Phase 3 and report the exact failure without
255+
retrying through a different command shape.
226256

227257
Never use `--no-verify`. Never amend as a workaround for a failed hook.
228258

@@ -249,21 +279,17 @@ git remote get-url --push origin
249279
Run only the command that matches the detected upstream state. Halt if the
250280
required push remote is missing.
251281

252-
When the push remote is on `github.com`, establish destination evidence in this
253-
same handoff turn immediately before the push:
254-
255-
```bash
256-
gh auth status -h github.com
257-
gh api user --jq .login
258-
gh repo view <owner>/<repo> --json viewerPermission --jq .viewerPermission
259-
```
282+
Treat the resolved push URL, current branch/upstream, and explicit `$handoff`
283+
invocation as the destination and user-authorization evidence. Show the push URL
284+
and branch in commentary immediately before requesting the push. Do not require
285+
`gh auth status`, `gh api user`, or `gh repo view` as push preconditions. Git and
286+
GitHub CLI may use different credentials. A `gh auth status` failure under
287+
network restriction is not evidence that the Git credential is invalid.
260288

261-
Derive `<owner>/<repo>` from the verified remote URL. Run these read-only GitHub
262-
checks with the network access they require; a failure from a network-restricted
263-
sandbox is not evidence that the stored credential is invalid. Never request or
264-
display a token. Continue only when the authenticated account and a `WRITE`,
265-
`MAINTAIN`, or `ADMIN` permission establish that the destination is authorized.
266-
Otherwise halt and report the exact failed check.
289+
Optional GitHub CLI checks must be read-only, run with the network access they
290+
require, and remain advisory. Never request or display a token. Do not halt
291+
before the authorized push solely because a GitHub CLI check is unavailable,
292+
network-blocked, unauthenticated, or inconclusive.
267293

268294
Push only if the handoff trigger or user instruction authorizes it. Use exactly
269295
one of these command shapes:
@@ -273,10 +299,14 @@ one of these command shapes:
273299

274300
Request the required escalation with the scoped approval prefix
275301
`["git", "push"]`. The justification must state that this is the explicitly
276-
authorized, non-force handoff push; name the verified remote, authenticated
277-
account and permission; and identify the current branch/upstream. Explicit
278-
`$handoff` invocation is normal push authorization for the standard handoff
279-
commit. Never force-push.
302+
authorized, non-force handoff push; name the verified push URL; and identify the
303+
current branch/upstream. Do not claim an authenticated account or repository
304+
permission unless it was actually verified. Explicit `$handoff` invocation is
305+
normal push authorization for the standard handoff commit. Never force-push.
306+
307+
The actual `git push` is the authoritative network and Git-authentication check.
308+
If it fails, report Git's exact error and do not reinterpret a preceding GitHub
309+
CLI result as proof of the cause.
280310

281311
If policy or automatic approval review rejects the push, halt Phase 4. Report
282312
the exact rejection and do not retry with another command shape, indirect

.codex/config.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ default_permissions = "game_studios"
44
extends = ":workspace"
55

66
[permissions.game_studios.filesystem.":workspace_roots"]
7+
".git" = "write"
8+
".agents" = "write"
9+
".codex" = "write"
710
".env*" = "deny"
811
"**/.env*" = "deny"
912

.codex/lib/validate_rules.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@
1717
"git_branch": "git-branch",
1818
}
1919

20-
REQUIRED_ENV_DENY_PATTERNS = {
21-
".env*",
22-
"**/.env*",
20+
REQUIRED_WORKSPACE_RULES = {
21+
".git": "write",
22+
".agents": "write",
23+
".codex": "write",
24+
".env*": "deny",
25+
"**/.env*": "deny",
2326
}
2427

2528
REQUIRED_FORBIDDEN_COMMAND_EXAMPLES = (
@@ -84,15 +87,15 @@ def main() -> int:
8487
'.codex/config.toml: missing [permissions.game_studios.filesystem.":workspace_roots"]'
8588
)
8689
else:
87-
missing_env_denies = sorted(
88-
pattern
89-
for pattern in REQUIRED_ENV_DENY_PATTERNS
90-
if workspace_rules.get(pattern) != "deny"
90+
incorrect_workspace_rules = sorted(
91+
f"{path}={workspace_rules.get(path)!r} (expected {access!r})"
92+
for path, access in REQUIRED_WORKSPACE_RULES.items()
93+
if workspace_rules.get(path) != access
9194
)
92-
if missing_env_denies:
95+
if incorrect_workspace_rules:
9396
errors.append(
94-
".codex/config.toml: missing strict workspace .env deny pattern(s): "
95-
+ ", ".join(missing_env_denies)
97+
".codex/config.toml: incorrect game_studios workspace rule(s): "
98+
+ ", ".join(incorrect_workspace_rules)
9699
)
97100
tui = data.get("tui")
98101
if isinstance(tui, dict) and "status_line" in tui:

.codex/lib/validate_runtime.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,49 @@
344344
),
345345
}
346346

347+
HANDOFF_GIT_CAPABILITY_REQUIRED_PHRASES = (
348+
"## Git Metadata Capability Gate",
349+
"Before Phase 0",
350+
"git rev-parse --absolute-git-dir",
351+
"test -w '<absolute-git-dir>'",
352+
"Do not request escalation for this check",
353+
"permission-profile configuration mismatch",
354+
"start a new session",
355+
"Do not begin the review gate",
356+
"normal workspace writes to `.agents/` and `.codex/`",
357+
"Do not tell the user to switch `/permissions` modes",
358+
)
359+
360+
HANDOFF_PHASE3_GIT_WRITE_REQUIRED_PHRASES = (
361+
"stage only the relevant paths by name",
362+
"Run staging without requesting escalation",
363+
"active `game_studios` profile grants Git metadata writes",
364+
"Commit with the standard handoff subject without requesting escalation",
365+
"without retrying through a different command shape",
366+
)
367+
368+
HANDOFF_PHASE4_PUSH_REQUIRED_PHRASES = (
369+
"Treat the resolved push URL, current branch/upstream, and explicit `$handoff` invocation",
370+
"Do not require `gh auth status`, `gh api user`, or `gh repo view` as push preconditions",
371+
"Git and GitHub CLI may use different credentials",
372+
"Do not halt before the authorized push solely because a GitHub CLI check",
373+
"name the verified push URL",
374+
"Do not claim an authenticated account or repository permission unless it was actually verified",
375+
"The actual `git push` is the authoritative network and Git-authentication check",
376+
"report Git's exact error",
377+
)
378+
379+
HANDOFF_PHASE4_PUSH_FORBIDDEN_PHRASES = (
380+
(
381+
"Continue only when the authenticated account",
382+
"mandatory GitHub CLI identity gate blocks the actual authorized Git push",
383+
),
384+
(
385+
"Otherwise halt and report the exact failed check",
386+
"mandatory GitHub CLI precheck failure still halts before Git push",
387+
),
388+
)
389+
347390
HANDOFF_REVIEW_FORBIDDEN_PATTERNS = (
348391
(
349392
re.compile(r"(?im)^\s*(?:\$\s*)?codex\s+(?:review|exec)\b"),
@@ -727,6 +770,58 @@ def validate_handoff_review_contract(root: Path) -> list[str]:
727770
skill_path = root / skill_rel
728771
if skill_path.exists():
729772
skill_text = skill_path.read_text(encoding="utf-8")
773+
missing = [
774+
phrase
775+
for phrase in HANDOFF_GIT_CAPABILITY_REQUIRED_PHRASES
776+
if not contains_phrase(skill_text, phrase)
777+
]
778+
if missing:
779+
errors.append(
780+
f"{skill_rel}: missing handoff Git capability phrase(s): "
781+
+ ", ".join(missing)
782+
)
783+
784+
phase3_match = re.search(
785+
r"(?ms)^## Phase 3: Commit Handoff\s*$\n(.*?)(?=^## Phase 4: Push Handoff\s*$)",
786+
skill_text,
787+
)
788+
if not phase3_match:
789+
errors.append(f"{skill_rel}: missing bounded Phase 3 commit section")
790+
else:
791+
phase3_text = phase3_match.group(1)
792+
missing = [
793+
phrase
794+
for phrase in HANDOFF_PHASE3_GIT_WRITE_REQUIRED_PHRASES
795+
if not contains_phrase(phase3_text, phrase)
796+
]
797+
if missing:
798+
errors.append(
799+
f"{skill_rel}: Phase 3 missing direct Git-write phrase(s): "
800+
+ ", ".join(missing)
801+
)
802+
803+
phase4_match = re.search(
804+
r"(?ms)^## Phase 4: Push Handoff\s*$\n(.*?)(?=^## Phase 5: Report And Stop\s*$)",
805+
skill_text,
806+
)
807+
if not phase4_match:
808+
errors.append(f"{skill_rel}: missing bounded Phase 4 push section")
809+
else:
810+
phase4_text = phase4_match.group(1)
811+
missing = [
812+
phrase
813+
for phrase in HANDOFF_PHASE4_PUSH_REQUIRED_PHRASES
814+
if not contains_phrase(phase4_text, phrase)
815+
]
816+
if missing:
817+
errors.append(
818+
f"{skill_rel}: Phase 4 missing direct push phrase(s): "
819+
+ ", ".join(missing)
820+
)
821+
for phrase, message in HANDOFF_PHASE4_PUSH_FORBIDDEN_PHRASES:
822+
if phrase in phase4_text:
823+
errors.append(f"{skill_rel}: Phase 4 {message}: {phrase!r}")
824+
730825
for pattern, message in HANDOFF_REVIEW_FORBIDDEN_PATTERNS:
731826
match = pattern.search(skill_text)
732827
if match:

README.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ The current release line is `v0.6.1`. It includes:
3333

3434
- Upstream-aware `$handoff` push routing: existing-upstream branches use plain
3535
`git push`, while only branches without an upstream set one through origin.
36-
- Same-turn GitHub authentication, destination, and repository-permission
37-
verification before a handoff push is attempted.
36+
- Same-turn destination verification from the configured push URL and
37+
branch/upstream. GitHub CLI identity checks are advisory because Git and `gh`
38+
can use different credentials; the authorized `git push` is the definitive
39+
network and Git-authentication check.
3840
- Scoped `git push` approval requests with fail-closed policy-denial handling
3941
and no alternate-command retries or workarounds.
4042

@@ -231,6 +233,27 @@ well as through the existing command rules and hooks, but it can also prevent
231233
Codex from creating or editing files such as `.env.example`. Use an
232234
agent-editable name such as `config.example` for non-secret templates.
233235

236+
The profile extends Codex's built-in `:workspace` profile but explicitly
237+
restores write access to `.git/`, `.agents/`, and `.codex/`. This preserves the
238+
upstream CCGS behavior required by Git handoff and skill/runtime maintenance
239+
while retaining the narrower `.env*` denials. Destructive command protections
240+
remain in `.codex/rules/settings.rules` and the validation hooks.
241+
242+
The project-local `default_permissions` setting is why `/permissions` displays
243+
`game_studios`; it does not edit the user's global `~/.codex/config.toml`.
244+
Approval modes control escalation separately and are not a repair mechanism for
245+
filesystem rules. `$handoff` checks Git metadata access before its review gate,
246+
then stages and commits without escalation; a networked push follows the active
247+
session's approval policy. It does not block an authorized push on inconclusive
248+
GitHub CLI authentication checks. If the initial Git check fails, install the
249+
current CCGS profile and start a new session so Codex resolves it. Do not switch
250+
approval modes, run `chmod`, or delete lock files as a workaround.
251+
252+
The distributable profile does not mix permission profiles with legacy
253+
`sandbox_mode`. After changing `.codex/config.toml`, start a new session before
254+
judging the resolved profile because active sessions retain their launch-time
255+
filesystem rules.
256+
234257
Default install behavior is patch-aware: a fresh target receives a full install,
235258
while a target with valid schema-v2 install state receives an incremental patch
236259
based on recorded package file hashes. Invalid, unsafe, or stale state aborts

docs/codex-conversion/codex-mapping-matrix.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ Official docs used: `https://developers.openai.com/codex/guides/agents-md`, `htt
6464
| `design/CLAUDE.md` | Design-document scope guidance | Nested Claude instructions | Root-routed Codex path guidance | `.codex/instructions/path-rules/design-directory.md` | replaced | partial | Preserve design and registry guidance; keep `design/registry/entities.yaml` shared. | Router-chain validation; no runtime `.claude` refs. |
6565
| `docs/CLAUDE.md` | Documentation scope guidance | Nested Claude instructions | Root-routed Codex path guidance | `.codex/instructions/path-rules/docs-directory.md` | replaced | partial | Preserve docs standards without depending on Claude imports. | Router-chain validation; instruction-budget check. |
6666
| `CCGS Skill Testing Framework/CLAUDE.md` | Test-framework specific guidance | Nested Claude instructions | Testing-framework docs/instructions | `CCGS Skill Testing Framework/AGENTS.md` or README section | replaced | semantic equivalent | Preserve the upstream framework path; replace Claude-specific guidance with Codex guidance. | File existence; no runtime `.claude` dependency. |
67-
| `.claude/settings.json` permissions | Allow/deny command and path policy | Claude permissions in settings JSON | Codex permission profile plus command rules | `.codex/config.toml`, `.codex/rules/settings.rules` | replaced | semantic equivalent | Use `[permissions.game_studios] extends = ":workspace"`; deny sensitive env paths; command policies go in `.rules`, not config. | TOML parse; rules lint; forbidden command fixture. |
67+
| `.claude/settings.json` permissions | Allow/deny command and path policy | Claude permissions in settings JSON | Codex permission profile plus command rules | `.codex/config.toml`, `.codex/rules/settings.rules` | replaced | semantic equivalent | Use `[permissions.game_studios] extends = ":workspace"`, then explicitly restore workspace writes to `.git`, `.agents`, and `.codex`; the inherited read-only carve-outs are stricter than upstream and break Git and CCGS maintenance workflows. Deny sensitive env paths; command policies go in `.rules`, not config. | TOML parse; rules lint; forbidden command fixture; fresh trusted session resolves `.git`, `.agents`, and `.codex` as writable while `.env*` remains denied. |
6868
| `.claude/settings.json` hooks | Hook event wiring | Claude settings hook blocks | Codex hooks config | `.codex/hooks.json` | replaced | semantic equivalent | Map supported lifecycle events only. Do not include Claude's `Notification` event; map notification behavior separately through user-level Codex notification docs. | JSON parse; event allowlist; command-path existence. |
6969
| `.claude/statusline.sh` | Always-on context/model/stage/session breadcrumb | Claude scriptable status line command | Codex native footer config plus startup/on-demand studio status | `.codex/config.toml` `[tui].status_line`, `.codex/hooks/studio-status-on-start.sh`, `.agents/skills/studio-status/SKILL.md` | replaced | partial; `Stage:` footer blocked | Preserve context/model visibility with Codex built-in `status_line` items. Preserve stage/review/session breadcrumb through shared-state parsing in `studio-status-on-start.sh` and `studio-status`; actual footer `Stage:` parity is blocked until Codex exposes a documented project custom footer item. | Config parse; status_line item allowlist; hook fixture; `studio-status` smoke reads `production/session-state/active.md`; no fake custom footer key. |
7070
| `.claude/agent-memory/lead-programmer/MEMORY.md` | Lead-programmer project memory seed | Claude agent memory file plus `memory: project` metadata | Repo-local Codex memory contract read by custom agent instructions | `.codex/agent-memory/lead-programmer/MEMORY.md` plus `.codex/agents/lead-programmer.toml` binding | ported | semantic equivalent | Rewrite Claude skill paths to `.agents/skills/<name>/SKILL.md` and keep canonical paths. Agent TOML must instruct `lead-programmer` to read the memory file before skill authoring, path canonicalization, and convention work. Do not write `~/.codex/memories`. | File existence; TOML binding scan; static scan: no writes to `~/.codex/memories`; content rewrite scan. |

docs/codex-conversion/corrected-architecture.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,9 +280,14 @@ Permissions:
280280
- `.codex/config.toml` should define:
281281
- `default_permissions = "game_studios"` only when safe to set in an owned config.
282282
- `[permissions.game_studios] extends = ":workspace"`.
283+
- Explicit write rules for `.git`, `.agents`, and `.codex`; upstream permits
284+
ordinary Git and runtime-maintenance workflows, so inherited read-only
285+
carve-outs are not parity.
283286
- Deny sensitive env paths.
284287
- Network disabled by default unless a user explicitly enables it.
285-
- `approval_policy = "on-request"` or documented granular equivalent.
288+
- Do not force a project-local `approval_policy`. Approval routing is
289+
user/session-owned; local staging and commits must work without escalation,
290+
while networked pushes follow the active session policy.
286291
- Do not set `sandbox_mode` in the same distributable config when permission profiles are used.
287292

288293
Command rules:

docs/codex-conversion/implementation-plan.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,11 @@ Acceptance criteria:
267267
- Hooks JSON parses and contains no `Notification`.
268268
- All hook scripts exist, are executable, and pass payload fixtures.
269269
- Config parses and does not mix permission profiles with `sandbox_mode`.
270+
- The `game_studios` profile explicitly grants write access to `.git`,
271+
`.agents`, and `.codex`, retains both `.env*` deny patterns, and does not set a
272+
project-local approval policy.
273+
- A fresh trusted session resolves those three runtime paths as writable;
274+
parser success alone is not permission-parity evidence.
270275
- If `[tui].status_line` is installed, all item IDs are Codex-supported built-ins and the project config parses under `--strict-config`.
271276
- Rules file passes positive/negative command examples.
272277

0 commit comments

Comments
 (0)