Skip to content

profile file: [[profile.NAME.file]] writes literal text read-only - #687

Merged
vyskocilm merged 2 commits into
mainfrom
feat/653-profile-file
Oct 22, 2026
Merged

vyskocilm merged 2 commits into
mainfrom
feat/653-profile-file

Conversation

@vyskocilm

@vyskocilm vyskocilm commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Closes #653.

New key [[profile.NAME.file]]: literal text as read-only file in sandbox.

[[profile.NAME.file]]
path = "/etc/gitconfig"
content = "..."
  • Rendered --perms 0444 --ro-bind-data, max 64 KiB. Inside: mode 444, owned by user, every write fails.
  • Content placeholders: {profile.NAME.identity.<leaf>} from include closure only, single-pass. {lbrace}/{rbrace} escapes, accepted in every sink's path vars.
  • p.Files ledger: which profile wrote which file.

Refused:

  • two different contents at one path (both profiles named); identical content joins
  • file under bind (masking)
  • file at path snug generates for selection; wording names layer to use, e.g. /etc/gitconfig instead of ~/.gitconfig
  • file at /tmp
  • file through symlink

--dry-run:

  • FILES block, full content; JSON files array
  • file displaced by snug's own: shown displaced, content not printed (refused dry-run leaked generated secret before fix)
  • NOT GRANTED says "profile X writes a file here" instead of blaming snug; heading reworded

Env coupling counts profile's own file as granted path, within include closure, downward only.

Other:

  • VisibleText dropped escaped trailing quote; now strconv.Quote.
  • CLAUDE.md invariant 1: rejectMasking exempts by Mount.Authored, not by KindData.
  • Docs: only rows TestBook* holds (profile-keys.md file row, {lbrace}/{rbrace} in path var table). Prose ## file section moved to book: document facts #673 removed from .claude/design #683 for manual review.

Goldens: profile-file.bwrap.txt, filesystem.profile-file.txt, json.profile-file.json, explain.profile-file.txt new; refusals.txt +4 entries, 1 reworded (join conflict "two different file contents (...); select one").

Review:

  • redteam round 1: F1 medium (displaced generated content printed on refused dry-run), F2 low (VisibleText quote). Both fixed, regression tests.
  • sandbox-tester: file at /tmp accepted; NOT GRANTED blamed snug for profile file's anchor dir. Both fixed, tests.
  • redteam round 2 on env coupling: no findings. Pre-existing low (path below single-file grant counts as granted) filed env coupling and not-granted mark count a path below a single-file grant as granted #686.

Checks:

  • make gate green.
  • make integration: new TestProfileFile* pass. TestOfflineHasOnlyLoopback, TestDoctorRunsCleanOnAHostThatCanRunSnug fail on dev host only: ip_gre loaded, fb_tunnels_only_for_init_net=0, unshare -rn ip -o link shows gre0 gretap0 erspan0 without snug.

🤖 Generated with Claude Code

vyskocilm and others added 2 commits October 22, 2026 11:49
Issue #653. A profile can now put a small file in the sandbox:

    [[profile.NAME.file]]
    path = "/etc/gitconfig"
    content = "..."

Rendered with --perms 0444 --ro-bind-data, up to 64 KiB. Content takes
{profile.NAME.identity.<leaf>} placeholders from the include closure,
expanded single-pass, plus {lbrace}/{rbrace}, which every sink's path
vars now accept. p.Files records which profile wrote each file.

Refused: two different contents at one path (both profiles named), a
file under a bind (masking), a file at a path snug generates for the
selection (wording names the layer to use instead, e.g. /etc/gitconfig
for ~/.gitconfig), a file at /tmp, a file through a symlink.

--dry-run gets a FILES block with full content (JSON: files array);
a file displaced by snug's own is shown as displaced, content not
printed. NOT GRANTED says when a profile writes a file there instead
of blaming snug, and its heading is reworded. Env coupling counts a
profile's own file as a path it grants, within its include closure.

VisibleText kept an escaped trailing quote by quoting with strconv.

CLAUDE.md invariant 1 corrected: rejectMasking exempts by
Mount.Authored, not by KindData.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Only the rows TestBook* holds stay: the `file` row in profile-keys.md
and {lbrace}/{rbrace} in the path variable table. The prose goes to
issue #683 for the maintainer to vet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@vyskocilm
vyskocilm merged commit 6058dac into main Oct 22, 2026
5 checks passed
@vyskocilm
vyskocilm deleted the feat/653-profile-file branch October 22, 2026 13:20
vyskocilm added a commit to miska/snug that referenced this pull request Oct 22, 2026
gomoni#687 profile-file placeholders enumerate every identity key, so the
refusal now names identity.gpg.fingerprint and identity.gpg.scope.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

profile: place literal text as a read-only file in the sandbox

1 participant