Skip to content

File permission bits in Kit not preserved when copied to sandbox #499

Open

Description

@rhcarvalho

I have a kit that copies an executable script (755 on the kit git repo) into the sandbox.

I believe this worked with sbx v0.35.0, which was the release at the time I tested the behavior and started using it.
This seems to have changed when I create sandboxes with the current version:

sbx version: v0.39.0 def8cb0523a77e757bdd6ef52b459fe374f3783e
● ! ls -l /home/agent/.gemini/antigravity-cli/statusline.sh
  ⎿  -rw-r--r-- 1 agent agent 5197 Aug 24 11:28 /home/agent/.gemini/antigravity-cli/statusline.sh

Was there an intentional change?

One change I made recently was migrating to the spec v2, but the docs don't say anything about permission bits for files in files/: https://github.com/docker/sbx-kits-contrib/blob/main/spec/SPEC-v2.md#58-files-directory

Files in the setup block can specify an explicit mode that defaults to 0644: https://github.com/docker/sbx-kits-contrib/blob/main/spec/SPEC-v2.md#56-setup

Activity

  1. self-assigned this
    on Aug 24, 2026
  2. kiview commented on Aug 24, 2026

    @kiview
    Member

    Hey @rhcarvalho, yes sorry, that's effectively a ~regression in spec v2, to align kit building on macOS and Windows to create the same kit artifacts. It's clearly not ideal for certain use cases, sorry for the inconvenience, we'll think how to deal with this going forward.

  3. added theissue type on Aug 24, 2026
  4. rhcarvalho commented on Aug 24, 2026

    @rhcarvalho
    Author

    @kiview thanks for confirming.

    FWIW I've worked around the issue by adding a setup.install step to explicitly set the permissions.
    praialabs/sbx-kits@4e3d6cf

  5. cdupuis commented on Aug 24, 2026

    @cdupuis

    Thanks for the precise report — this was an intentional change, but the consequence for kits shipping executable scripts was neither intended nor documented. Your version bisect is exactly right.

    What changed

    Two separate pins are involved:

    1. Since v2 support landed (well before v0.35.0), the v2 layer packer stamps every file entry with mode 0644. On-disk permission bits are discarded at pack time.
    2. In v0.39.0 that pin was extended to local-directory and git loads, which until then kept the author's on-disk mode.

    Your kit is git-sourced, so on v0.35.0 it took path 2 and the 0755 survived — while the same kit pushed to a registry and pulled back would already have been 0644. v0.39.0 closed that inconsistency so a kit injects identically whether it comes from git, a local directory, or an OCI pull. Staying on schemaVersion: "1" would have avoided it, since the pin is v2-only.

    Why the mode is pinned

    The v2 layer is content-addressed and signable, so its tar bytes have to be reproducible from the source tree alone. On-disk modes aren't: they vary with the author's umask, and on Windows Go cannot report POSIX bits at all — os.Stat derives permissions from the read-only attribute alone, so every writable file reads as 0666 and a regular file never carries an execute bit. If the packer honored on-disk modes, the same kit packed on Windows and on Linux would produce different layer digests for every file, not just scripts.

    The trade-off is that v2 has no way today to declare a per-file executable bit for files/.

    Workaround

    setup.install runs after files/home/ is staged, so a chmod there works:

    setup:
      install:
        - command: "chmod 0755 /home/agent/.gemini/antigravity-cli/statusline.sh"
          user: "0"
          description: Restore the executable bit dropped by the v2 file-mode pin

    Verified against a real sandbox: without the install command the script lands 0644 and is not executable by the agent user; with it, 0755 and it runs.

    Two caveats:

    • This works for files/home/ only. files/workspace/ is written after the workspace is populated, which is after install commands run, so the same chmod against a workspace path would fail.
    • Install-command failures are non-fatal at create time, so a wrong path leaves you with a still-0644 file rather than a hard error.

    On our side

    You're right that this isn't documented — SPEC-v2.md §5.8 covers targets, path rules and composition but says nothing about permission bits, so there was no way to anticipate this from the spec. That's a gap worth fixing, along with the question of whether files/ should gain an explicit mode declaration so kits don't need the chmod workaround.

  6. kiview commented on Aug 24, 2026

    @kiview
    Member

    @rhcarvalho FWIW, I think the install step approach is a great one for now and very portable 👍

  7. rhcarvalho commented on Aug 24, 2026

    @rhcarvalho
    Author

    @cdupuis thanks for the details, it makes perfect sense.

    • Install-command failures are non-fatal at create time, so a wrong path leaves you with a still-0644 file rather than a hard error.

    Oh, I was not aware. This is also not documented.

    I'd certainly rather the install command failed and I get to fix it than silently creating the sandbox in an inconsistent state. This would be specially true if one uses multiple commands in the setup.install list. If ignoring exit status was desired, kit authors could do so explicitly.

  8. rhcarvalho commented on Aug 24, 2026

    @rhcarvalho
    Author

    @rhcarvalho FWIW, I think the install step approach is a great one for now and very portable 👍

    In the recent past I've inlined files from files/home/ into spec.yaml, then eventually reverted that change. The original motivation was to centralize what goes into the new sandbox and control permissions, because setup.files supports explicit mode.

    Only one of the files I generate depends on the ${WORKDIR} placeholder replacement. I decided to revert the inlining because editing/maintaining larger files inside of yaml was too inconvenient.

    So one idea I can offer is to expand the setup.files spec to allow pointing to the file contents relative to the spec.yaml file. This way another solution to the permission issue would be:

    spec.yaml:

    # ...
    setup:
      # ...
      files:
        - path: "/home/agent/.gemini/antigravity-cli/settings.json"
          onlyIfMissing: false
          mode: "0600"
          content: |
            {
              "trustedWorkspaces": [
                "${WORKDIR}"
              ]
            }
        - path: "/home/agent/.gemini/antigravity-cli/statusline.sh"
          onlyIfMissing: false
          mode: "0755"
          source: "./statusline.sh"

    (Didn't think the name source through.)

    This is similar to the Docker Compose spec that originally only supported pointing to a Dockerfile file, then later gained the inline form. In our case here, setup.files support the inline form, but not pointing to a file in disk for the contents.

  9. rltas commented on Aug 24, 2026

    @rltas

    I came across this because it broke https://github.com/docker/sbx-kits-contrib/tree/main/claude-sbx-statusline and potentially other community contributed kits as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions