Skip to content

fix(init): confine .bmad-loop, policy.toml and .gitignore writes (#771) - #829

Merged
pbean merged 2 commits into
fix/769-central-config-compatfrom
fix/771-init-write-containment
Sep 23, 2026
Merged

pbean merged 2 commits into
fix/769-central-config-compatfrom
fix/771-init-write-containment

Conversation

@pbean

@pbean pbean commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #771.

Summary

  • install_into guarded only the hook-config and skill-tree writes. The .bmad-loop mkdir, the fallback policy.toml write and the .gitignore append all followed whatever link was at that name. A symlink or junction pointing out of the project, or a dangling policy.toml link (which fails is_file()), made init write outside the project tree and still report success.
  • All three destinations are now checked with _confined_to before init's first write. If one escapes, init prints FAIL naming the path and returns 1. No hook config, skills, policy or .gitignore change is left behind.
  • The check requires each path to be strictly below the project root. For .bmad-loop, a directory that resolves to the root itself would put policy.toml at the top level, and checking only the policy path misses that. policy.toml and .gitignore name files, so the root directory itself is never a valid target. A link that stays inside the project still passes, and the write goes through it.
  • The .gitignore append is still an append, now opened by its resolved name, so it keeps the file's mode and leaves an in-project link as a link. The new policy still uses write_text, not atomic_write_text. That branch runs only when no regular file exists, so a torn write loses nothing the operator owns (Non-atomic read-modify-rewrite in ~10 places; a short write truncates ledgers, specs, and the board #379's concern). atomic_write_text would also create the file as 0600 instead of the umask default.

Notes for reviewers

  • Compatibility note: init now refuses when .bmad-loop or an existing policy.toml resolves outside the project, even when it would write nothing there. Operators who keep either one outside the project tree (for example, symlinked to a shared location) will see init fail until they move it inside the project.
  • The Windows CI leg is the proof for the junction case. test_init_refuses_a_bmad_loop_dir_junctioned_out_of_the_project runs only on win32 and is skipped on Linux by design. The symlink cases are POSIX-only.
  • Gate at this layer: 10947 passed, 86 skipped; pyright 0 errors.

Stack

Part of a stack; merge bottom-up. This is layer 4 of 8, on top of #828.

Stack order (bottom → top): #826 → #827 → #828 → #829 → #830 → #831 → #832 → #833.

Summary by CodeRabbit

  • Bug Fixes

    • init now refuses to proceed when .bmad-loop, policy.toml, or .gitignore resolves outside the project directory or to the project root.
    • Failed initialization no longer writes partial setup state.
    • .gitignore updates preserve existing content, permissions, and repeat-run behavior when using valid in-project paths.
  • Documentation

    • Added a changelog entry describing the initialization safety checks.

@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Warning

Review limit reached

Next included review available in 24 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: dabd4071-9e52-4f1a-be72-eec6d43d69b1

📥 Commits

Reviewing files that changed from the base of the PR and between 54a8d7d and 8f4da43.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/bmad_loop/install.py
  • tests/test_install.py

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: f6b10894-ff73-41b6-9530-914b08c7ce22

📥 Commits

Reviewing files that changed from the base of the PR and between c1ff6c6 and 54a8d7d.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/bmad_loop/install.py
  • tests/test_install.py

Included review availability: Your plan provides up to 2 included reviews per hour; 0 remain after this review.


Walkthrough

install_into now checks that .bmad-loop, policy.toml, and .gitignore resolve inside the project before writing. Tests cover escaping links, accepted in-project links, content and mode preservation, and idempotent reruns. The changelog records the behavior.

Changes

Init containment

Layer / File(s) Summary
Validate resolved init targets
src/bmad_loop/install.py
install_into rejects targets that resolve outside the project before writing. Policy creation and .gitignore updates use the validated paths.
Cover refusal and accepted paths
tests/test_install.py, CHANGELOG.md
Tests verify refusal without state changes, accepted in-project links, mode and content preservation, and idempotency. The changelog records the refusal behavior.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: dracic

Merge Risk: ⚪ Minimal · up to 54a8d

The changed init behavior rejects escaping targets while preserving supported in-project links; no merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 2 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Issue #771 requires confinement for the relay script, fallback policy.toml, and root .gitignore, including symlink and junction escapes, with no setup writes after refusal. The PR summary states t…
Out of Scope Changes check ✅ Passed The changed files are src/bmad_loop/install.py, tests/test_install.py, and CHANGELOG.md. The implementation addresses issue #771 directly. The tests verify the required security behavior. The ch…
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: confining init writes for .bmad-loop, policy.toml, and .gitignore.
Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 2 files. (1 skipped: 1 unsupported.)

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit checks each path with care
No wandering writes escape the lair
Safe links stay, and modes remain
Repeat the run: no doubled grain
The changelog marks the guarded gate

Comment @coderabbitai help to get the list of available commands.

@pbean

pbean commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-23T03:01:04.568625Z 8f4da43 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2d2729349b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tests/test_install.py Outdated
Comment on lines +1362 to +1365
project, outside = tmp_path / "proj", tmp_path / "outside"
project.mkdir()
outside.mkdir()
(project / ".bmad-loop").symlink_to(outside, target_is_directory=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use the sandbox fixture for init confinement tests

These new tests hand-roll project directories under tmp_path, and the remaining added cases repeat the same pattern. This bypasses the shared project fixture’s Git repository and BMAD-shaped baseline, so the tests can miss interactions with the initialized repository and silently drift as the canonical sandbox changes; adapt the fixture copy for each symlink/junction scenario instead.

AGENTS.md reference: AGENTS.md:L58-L60

Useful? React with 👍 / 👎.

@pbean
pbean force-pushed the fix/771-init-write-containment branch from 2d27293 to 7e37359 Compare September 23, 2026 01:41
@pbean
pbean force-pushed the fix/771-init-write-containment branch from 7e37359 to 54a8d7d Compare September 23, 2026 02:17
install_into guarded only the hook-config and skill-tree writes. The
.bmad-loop mkdir, the fallback policy.toml write and the .gitignore append
went through whatever link sat at the name. A symlink or junction out of the
project, or a dangling policy.toml link that fails is_file(), made init write
outside the tree and still report success.

All three destinations are now checked with _confined_to before init's first
write. An escape prints FAIL naming the path and returns 1, with no hook
config, skills, policy or gitignore change left behind.

Strictly-below is the right predicate for each path:
- .bmad-loop: a directory that resolves to the root would drop policy.toml
  at top level. The policy check alone misses that, because root/policy.toml
  is still below the root.
- policy.toml and .gitignore: these name files, and the root is a directory
  no write should aim at.

An in-project link still passes and is written through.

The .gitignore append stays an append, now opened by its resolved name, so it
keeps the file mode and leaves an in-project link a link. The new policy keeps
write_text rather than atomic_write_text. That branch runs only when no
regular file exists, so a torn write loses nothing the operator owns (#379's
concern). atomic_write_text would also mint the file 0600 instead of the
umask default.
@pbean
pbean force-pushed the fix/771-init-write-containment branch from 54a8d7d to 9e209bb Compare September 23, 2026 02:39
…dbox

The refusal rows now snapshot the sandbox's git status (ignored paths
included) after planting the link and require it unchanged after the
refused init, instead of probing a hand-rolled tmp_path tree.
@pbean

pbean commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: 8f4da43b0b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@pbean
pbean merged commit ddd2bf8 into main Sep 23, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant