Skip to content

A chat whose project folder is inside a Git-ignored directory cannot be saved or moved #2539

Open

Description

@AbirAbbas

What happened

On dev@b5b7dead1, a chat whose project folder lies inside a directory that an enclosing Git repository ignores cannot be saved. While it runs, the window says:

calls are not being sealed, so rewind cannot reach them: screen for secrets: cannot read which folders Git ignores here: exit status 128

On quit:

sync: closing a chat: cannot read which folders Git ignores here: exit status 128
codeaf could not save this chat; unsaved turns are kept in <home>/v3/projects/<project>/<id>/.cell/transcript.jsonl and return on the next start if this computer still holds the chat.

The process exits 1, and the chat never reaches the other computer. The same happens with GIT_CEILING_DIRECTORIES set to the project folder itself, which Git does not treat as a fence. Setting the ceiling to an ancestor above the enclosing repository avoids it.

The immediate cause is Git itself. From inside an ignored directory, this plumbing call exits 128 (seen with Git 2.50.1):

$ git --no-optional-locks -C <repo>/w/p ls-files --others --ignored --exclude-standard --directory -z
fatal: git ls-files: internal error - directory entry not superset of prefix

codeaf treats that unexpected failure as "cannot read which folders Git ignores here", deliberately, so as not to capture ignored private files. The chat therefore cannot be sealed at all.

Replication

Deterministic (no model). The Git step alone:

mkdir -p /tmp/ig/g/w/p && git init -q /tmp/ig/g
printf 'w/\nw/p/secret.txt\n' > /tmp/ig/g/.gitignore
printf 'CONFIDENTIAL-MARKER\n' > /tmp/ig/g/w/p/secret.txt
git --no-optional-locks -C /tmp/ig/g/w/p ls-files --others --ignored --exclude-standard --directory -z; echo "exit $?"

Through codeaf, with the keyless stub (go build -o /tmp/ig/stub ./test/remote/stub, run with -addr 127.0.0.1:18842, OPENROUTER_API_KEY=stub CODEAF_BASE_URL=http://127.0.0.1:18842/api/v1) and a Go relay (bin/codeaf relay --listen 127.0.0.1:18843 --store /tmp/ig/store, CODEAF_SYNC_URL=http://127.0.0.1:18843):

  1. Pair two fresh homes. Only the approving home opens codeaf first; the joining home must never have run it (see A computer that opened codeaf once cannot join another: 'this computer has local work' with no chats #2538).
  2. On the first home, run bin/codeaf chat --no-host --model stub/scripted, then type /project /tmp/ig/g/w/p and PROBE-ECHO ignored-workspace-words.
  3. Quit. The sealing refusal and the "could not save this chat" receipt appear, the exit status is 1, and the second home never receives the chat.

Where

ignoredDirectories in internal/cellstore/ignored.go: the ls-files --others --ignored --exclude-standard --directory call, and its handling of an unexpected Git failure as ignoredReadFailure. Its callers in the secret screen and seal path then refuse to seal.

The fix

A project folder inside an ignored directory is still a chat that saves and moves. Read Git's ignore answer in a form Git can give from that position: for example, run the listing from the repository's top level with the project folder as a pathspec, or first ask git check-ignore about the folder itself. Apply the result: files Git ignores stay out of saved history, including an ignored secret.txt. The chat's own words always save. A genuinely unreadable answer still refuses rather than capturing ignored files.

Findings from a first attempt

A codeaf-only workaround was tried in #2540 and taken back out. Asking Git about the folder first, then keeping an ignored project's top-level entries out by listing them as exclusions, made the save work in the simple case. A review then found four problems:

  • The project's own .furrowpolicy entered saved history unscreened. The engine's policy parser forbids excluding that file.
  • The chat was still lost when that .furrowpolicy turns on gitignore. The engine unions both policies, and for an ignored root its tracked_children returns nothing. That drops the composed .cell entry; this is A wholly Git-ignored workspace drops the composed .cell entry from the capture furrow#28.
  • One rule per top-level entry breaks the engine's limit of 1,024 rules and 64 KiB per policy.
  • Recognising "no repository" from Git's English message breaks on non-English Git, and the extra Git calls added several processes to every save.

So the fix needs the engine to keep the composed conversation when Git ignores the whole folder (furrow#28). After that, codeaf's part is to ask Git about the folder in a form Git can answer, and to keep the refusal for a genuine Git failure.

Acceptance

  • e2e: the replication above quits with exit 0, the second home continues the chat with STUB-REPLY: PROBE-ECHO ignored-workspace-words, and CONFIDENTIAL-MARKER appears nowhere in the second home.
  • e2e: controls: an ancestor ceiling, and an ordinary project that is its own repository, behave as today.
  • Unit: the ignore reading for a folder inside an ignored directory returns the ignored paths without error. A real Git failure still yields cannot read which folders Git ignores here.

Activity

  1. added a commit that references this issue on Nov 9, 2026
    e7f9571
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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions