Skip to content

Hashless Codex journal can overwrite later settings and become trusted on reinjection #4005

Closed

Description

@luvs01

Client or integration

Other

Area

Other

Summary

A Codex journal without recorded injected-state hashes can overwrite later native configuration edits or delete a later profile. restoreJournalState() currently treats a missing hash as evidence that the current artifact is unchanged, then restores the saved original. This state can be produced by a legacy record or an interruption between writeJournal() and markJournalInjectedState().

There is a second path: reinjection over a routed, hashless journal keeps its older original but assigns the new injected configuration's hash to it. A later restore can then accept that old original as if the new configuration belonged to the old snapshot. Native-restore and reconcile callers also need to distinguish an unverified snapshot from a verified partial restore.

Refs #2948. This is a reproducible narrower defect in the current source; it does not establish the cause of that historical machine's shutdowns or justify treating every restore log line as a configuration overwrite.

Reproduction

Use an isolated Codex home with the current dev source:

  1. Write a native config.toml, call writeJournal(), and do not call markJournalInjectedState().
  2. Change the config to another native configuration, adding a different user-selected model. Calling restoreJournalState() overwrites those new bytes with the old original and reports completion.
  3. In a separate case, create a profile after the snapshot. Restoration deletes that new profile because the snapshot recorded no original profile and no injected profile hash.
  4. For the reinjection case, place recognizable routed configuration plus new user settings over the same hashless snapshot. Current preflight accepts it and injection can fill the missing hash while retaining the older original.

The prepared regressions use temporary config/profile files and mock/local execution only. The proposed policy keeps a changed file and the journal when that file lacks its own injection hash, accepts already-original files without rewriting them, and permits a genuinely native configuration to establish a new snapshot. Routed reinjection must verify the retained snapshot before writing.

Version

402be7c; Bun 1.4.0 (34cbb9a40)

Operating system

Microsoft Windows 11 Pro, version 10.0.26200 (build 26200)

Provider and model

Not provider-specific. No live account, provider request, or reset credit is used.

Logs or error output

hashless interrupted snapshot preserves later native config edits:
  expected current user settings; received the earlier original config
hashless interrupted snapshot preserves a later profile:
  expected the current profile; received null
hashless native restore refuses instead of reporting an uncertain snapshot as restored:
  expected success=false; received true
hashless routed snapshot is not promoted by reinjection after user edits:
  expected preflight.success=false; received true

Six regressions failed against unchanged base source, including redundant rewriting and loss of an empty original profile's presence. The proposed correction preserves empty files separately from absent files.

Screenshots and supporting files

A focused source/test/documentation pull request will link this issue. This is a filesystem-state defect; no screenshot is needed to reproduce it.

Redacted configuration

model_provider = "openai"
model = "user-selected-model"

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Activity

  1. lidge-jun commented on Sep 8, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 68 / 80

    이 이슈는 Codex 저널에 injectedConfigHash / injectedProfileHash가 없는(해시 없는) 스냅샷이 생기면, 그 뒤에 사용자가 고친 네이티브 설정이나 새로 만든 프로필을 restoreJournalState()가 ‘우리 주입이 그대로다’라고 잘못 보고 예전 원본으로 덮어쓰거나 지워 버리는 구멍입니다. 지금 dev HEAD 402be7c1f(package 2.48.0)의 src/codex/journal.ts를 보면 그 판단이 한 줄에 드러납니다. config는 !journal.injectedConfigHash || sha256(currentConfig) === journal.injectedConfigHash 이고, profile은 journal.injectedProfileHash === undefined || … 입니다. 해시가 없으면 왼쪽 조건이 바로 참이 되어 ‘변경 없음’으로 취급하고, 원본 config.toml을 다시 쓰거나(원본에 프로필이 없었으면) 나중에 생긴 프로필 파일을 지웁니다.

    재현도 HEAD 기준으로 좁고 깨끗합니다. 격리된 Codex 홈에서 writeJournal()만 하고 markJournalInjectedState()를 건너뛴 뒤(중단·레거시 기록과 같은 상태), 설정을 바꾸고 다른 모델을 고르거나 프로필을 만든 다음 restore를 돌리면 새 바이트가 사라지고 ‘복원 완료’로 보고됩니다. 작성자가 붙인 실패 로그(expected current user settings / expected the current profile / expected success=false)가 그 계약을 그대로 보여 줍니다. 닫힌 이슈 #2948(임의로 오래된 설정을 되살리는 큰 이야기)을 참고만 하고, 그 역사적 원인이나 모든 restore 로그 줄을 덮어쓰기라고 단정하지 않은 점도 좋습니다. 여기는 ‘해시 검증 없는 스냅샷’이라는 지금 소스의 좁은 결함입니다.

    두 번째 경로는 더 교묘합니다. 해시 없는 저널 위에 라우팅된(주입된) 설정을 다시 주입하면, markJournalInjectedState의 injectedConfigHash ??= sha256(config)가 비어 있던 자리에 새 주입 해시를 채우면서 originalConfig는 예전 스냅샷을 그대로 둡니다. 이후 restore는 ‘지금 파일이 그 새 해시와 같다’고 보고 예전 원본을 신뢰된 복원으로 받아들입니다. 즉 검증되지 않은 스냅샷이 reinjection만으로 ‘검증된 것처럼’ 승격됩니다. 제안 정책(해시 없는 파일이 바뀌었으면 파일과 저널을 유지, 이미 원본이면 다시 쓰지 않음, 진짜 네이티브만 새 스냅샷 허용, 라우팅 reinjection은 보존된 스냅샷을 먼저 검증)은 현재 코드의 writeJournal 네이티브 전제·??= 첫 해시 고정 의도와도 맞습니다.

    호출부도 같이 봐야 합니다. restoreJournal()은 restoreJournalState().complete만 보고, reconcileJournal()은 죽은 PID/다른 클라이언트일 때 restore를 돌린 뒤 경고를 찍습니다. 해시 없는 스냅샷을 ‘완전 복원 성공’으로 보고하면 저널이 지워지고(if (complete) removeJournal()), 나중에는 무엇을 덮어썼는지 추적할 증거도 사라집니다. 그래서 네이티브 restore와 reconcile 호출자가 ‘검증된 부분 복원’과 ‘검증 안 된 스냅샷 거절’을 구분해야 한다는 이슈의 요구는 맞습니다. 빈 원본 프로필과 ‘원래 프로필 파일 자체가 없음’을 구분하자는 점도, 지금 originalProfile: null이면 존재하던 프로필을 지우는 분기와 직결됩니다.

    우선순위 68로 둔 이유는, 매 요청마다 터지는 프록시 버그는 아니지만 unclean shutdown·주입 중단 창에서 사용자 config.toml / 프로필을 되돌리거나 지울 수 있는 데이터 손실형이기 때문입니다. types.ts/config.ts 분할 캠페인과 무관하고, 지금 열린 짝 PR은 아직 이 이슈 번호로 연결되어 있지 않습니다(작성자가 곧 소스·테스트·문서 PR을 링크하겠다고 했습니다). 테스트는 임시 설정/프로필과 mock/local만 쓰겠다고 해서 실계정·reset credit 오염 위험도 없습니다.

    경로 src/codex/journal.ts restoreJournalState 약 215행 - 해시 부재를 unchanged로 취급해 원본을 덮어쓴다.
    경로 src/codex/journal.ts markJournalInjectedState 약 144행 - 해시 없는 저널에 새 주입 해시만 채우면 옛 original이 ‘검증된 스냅샷’처럼 승격된다.
    경로 src/codex/journal.ts reconcileJournal / restoreJournal - complete=true면 저널을 지워 증거까지 사라질 수 있다.
    관련 #2948 - 이미 닫힌 큰 복원 이야기와 겹치지만, 이 이슈는 HEAD의 해시 검증 구멍만 겨냥하므로 중복으로 닫지 않는다.

    메인테이너의 판단이 필요한 지점

    • 해시 없는 스냅샷을 거절(success=false·저널 유지)할지, 소유 필드 폴백만으로 부분 복원을 허용할지
    • 레거시 해시 없는 저널을 한 번 만나면 폐기할지, 네이티브로 확인된 경우만 새 스냅샷으로 갈아끼울지
    • 빈 파일 vs 파일 부재를 journal 스키마(version 1)에 어떻게 기록할지

    너의 추천
    이슈는 open으로 두고, 작성자가 올리는 회귀 테스트 포함 PR을 기다린다. 머지 조건의 최소안은 (1) 해시 없는 config/profile은 ‘변경 없음’으로 쓰지 말 것, (2) reinjection이 해시 없는 스냅샷을 승격하지 말 것, (3) restore/reconcile이 미검증 스냅샷을 complete 성공으로 보고하며 저널을 지우지 말 것이다. PR이 오면 #2948 재오픈이 아니라 이 이슈만 Closes로 묶으면 된다.

    이 댓글은 grok-bot이 작성했습니다

  2. added a commit that references this issue on Sep 8, 2026
    cfff026
  3. lidge-jun commented on Sep 8, 2026

    @lidge-jun
    Owner

    Fixed on dev in cfff026 (via #4049, carrying #4006 by @luvs01).

    A journal entry without injectedConfigHash no longer counts as proof the file is unmodified. restoreJournalState now reports unverified and preserves both the current files and the journal instead of overwriting your configuration with the saved original, and injection refuses to write when the journal has no verified baseline. Recovery for that case is documented in the Codex integration guide under "Recovery without injection hashes". Homes whose journals carry hashes — written since c29ee783e (2026-06-27) — are unaffected.

    Verified at the merged head: bun test tests/codex-integration/codex-journal.test.ts passes, and Cross-platform CI run 34256853348 was green across 19 jobs.

    Closing manually because pull requests here target dev, and GitHub only auto-closes on merge into main.

  4. lidge-jun commented on Sep 8, 2026

    @lidge-jun
    Owner

    Landed via #4049 at cfff026

  5. added a commit that references this issue on Sep 17, 2026
    3568058
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinglanded-via-maintainerOriginal PR closed after landing via a maintainer merge train

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions