Visitar URL original
fix(config-write): skip redundant .gitignore append when existing rule covers path (#1484) by amasen02 · Pull Request #1485 · schubydoo/clauster · GitHub
Skip to content

fix(config-write): skip redundant .gitignore append when existing rule covers path (#1484) - #1485

Merged
schubydoo merged 5 commits into
schubydoo:mainfrom
amasen02:fix/config-write-skip-redundant-gitignore-1484
Sep 4, 2026
Merged

schubydoo merged 5 commits into
schubydoo:mainfrom
amasen02:fix/config-write-skip-redundant-gitignore-1484

Conversation

@amasen02

@amasen02 amasen02 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #1484 — checks whether an existing rule in .gitignore already covers the target path before appending, skipping redundant appends for covered files and their backup siblings.

Problem

When writing local-scope files (e.g. .claude/settings.local.json), ensure_gitignored previously only checked for an exact line match against path (and path.bak). If an existing ancestor rule like /.claude, .claude/, or .claude already ignored the entire directory, ensure_gitignored still appended redundant entries to .gitignore, creating unexpected working tree modifications on main.

Solution

  1. Added _is_gitignored_by_existing_rules(relative_path: str, existing_content: str) -> bool in src/clauster/config_write.py.
  2. Matches paths against:
    • Root-anchored directory rules (/.claude, /a/b, /.claude/)
    • Unanchored directory rules (.claude, .claude/, foo/, a/b/)
    • Exact line matches with or without leading slashes
    • Basename wildcard rules (e.g. *.bak)
  3. Updated ensure_gitignored to filter wanted entries through _is_gitignored_by_existing_rules, making covered files a clean no-op.
  4. Added test cases to tests/test_config_write.py verifying:
    • No-op when root-anchored ancestor rule /.claude is present.
    • No-op when trailing-slash ancestor rule .claude/ is present.
    • No-op when bare ancestor rule .claude is present.
    • No-op when nested ancestor directory rule a/b/ is present.
    • Appending continues to work as expected for uncovered paths.
    • Backup sibling handling with wildcard *.bak.

Verification

  • Ran pytest tests/test_config_write.py: 166 passed, 0 failed.
  • Staged as Draft PR for maintainer review.

@schubydoo

Copy link
Copy Markdown
Owner

@claude review

@claude

claude Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

Claude finished @schubydoo's task in 4m 26s —— View job


Review complete

  • Read the review instructions from the base branch
  • Read the diff and the surrounding config_write.py context
  • Check the call paths that depend on ensure_gitignored
  • Read the new tests
  • Submit one grouped review

Verdict: 1 important, 4 nits. The ancestor-rule cases work, but one branch can now skip the append for a file git will still track.

See the review for the findings and the inline threads.
· branch

@claude claude 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.

1 important, 4 nits

The helper handles the four shapes in the description correctly (/.claude, .claude/, .claude, a/b/). One case turns it from conservative into fail-open, which matters because the callers treat this function as the guarantee that a secret-bearing file stays untracked (gitignore-on-create, #766).

Finding Severity Location
A ! negation line can leave settings.local.json tracked, because the wildcard branch still reports "covered" 🔴 Important src/clauster/config_write.py:1565
fnmatch.fnmatch folds case on Windows, so one .gitignore gives two answers 🟡 Nit src/clauster/config_write.py:1594
The docstring drops the "conservative, not a real matcher" caveat, which is still true 🟡 Nit src/clauster/config_write.py:1524
Three subsumed conditions never change a result 🟡 Nit src/clauster/config_write.py:1578
No changeset and no no-changelog label, but the change is user-facing 🟡 Nit .changeset/

How I verified. By reading, against the gitignore precedence rules and the three call paths that depend on this function (config_write_settings.py:339, config_write_hooks.py:300, app.py:3712). I did not run the suite. That is the design here: CI is the measurement, across three operating systems.

Not flagged. The six new tests read correctly and pin the shapes the description claims. No docs/ page describes this behavior, so no documentation update is due.

Comment thread src/clauster/config_write.py Outdated
Comment thread src/clauster/config_write.py Outdated
Comment thread src/clauster/config_write.py
Comment thread src/clauster/config_write.py Outdated
@amasen02
amasen02 marked this pull request as ready for review September 4, 2026 02:39
@amasen02
amasen02 requested a review from schubydoo as a code owner September 4, 2026 02:39
@schubydoo

Copy link
Copy Markdown
Owner

Hi @amasen02, thanks for your contribution. When you have a moment, can you address the feedback comments from Claude above?

@amasen02

amasen02 commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor Author

Hey @schubydoo, thanks for taking a look! Just pushed an update addressing all the review points:

  • Added top-to-bottom handling for ! negation lines so negated patterns properly un-ignore and append as expected
  • Switched to fnmatch.fnmatchcase so Windows and Linux evaluation stays strictly case-sensitive
  • Cleaned up redundant subsumed checks and clarified the conservative matcher docstrings
  • Added unit tests for negation overrides and case-sensitivity
  • Added the .changeset entry

Tests are green locally. Let me know if anything else is needed!

@codecov

codecov Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@schubydoo

Copy link
Copy Markdown
Owner

@claude review

@claude

claude Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

Claude finished @schubydoo's task in 3m 47s —— View job


Re-review complete

  • Read the review instructions from the base branch
  • Read the new diff and the current config_write.py
  • Reconcile the prior Important finding (negation lines)
  • Read the new tests and the changeset
  • Submit one grouped review

Verdict: 1 important, 0 nits. All five prior findings are fixed. One residual case of the same class remains open.

See the review for the finding and the inline thread.
· branch

@claude claude 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.

1 important, 0 nits

Previous findings

  • 🔴 A ! negation line can leave the file tracked — FIXED. The scan at lines 1592-1604 now reads every line top to bottom and lets the last match win, so a negation clears the flag. test_ensure_gitignored_appends_when_negation_line_overrides_wildcard pins the exact case from that finding.
  • 🟡 fnmatch case folding — FIXED, line 1587 uses fnmatch.fnmatchcase.
  • 🟡 Docstring caveat — FIXED, lines 1549-1552 restore the "conservative, not a full gitignore engine" limit.
  • 🟡 Subsumed conditions — FIXED, the ancestors list and the three redundant tests are gone.
  • 🟡 Missing changeset — FIXED, .changeset/config-write-skip-redundant-gitignore.md.

New findings

Finding Severity Location
A negation pattern that contains a slash is never parsed, so the basename-wildcard branch still answers "covered" for a file git tracks 🔴 Important src/clauster/config_write.py:1587

This is the same class as the previous Important finding, with a narrower trigger. The top-to-bottom scan closed the plain form (!settings.local.json). It did not close the globbed form (!**/settings.local.json, !.claude/*.json), because _rule_matches returns False for every negation pattern that contains a slash and is not a directory prefix.

How I verified. By reading, against the gitignore precedence rules and the five call paths that depend on this function (config_write_settings.py:339, config_write_hooks.py:300, config_write_permissions.py:209, config_write_skills.py:703, claude_md.py:358). I did not run the suite. That is the design here: CI is the measurement, across three operating systems.

Not flagged. The two new tests cover the negation and case-sensitivity fixes and read correctly. The changeset is one line and patch is the right bump. No docs/ page describes this behavior, so no documentation update is due.

Comment thread src/clauster/config_write.py
@schubydoo

Copy link
Copy Markdown
Owner

Hey @schubydoo, thanks for taking a look! Just pushed an update addressing all the review points:

  • Added top-to-bottom handling for !\ negation lines so negated patterns properly un-ignore and append as expected
  • Switched to \ nmatch.fnmatchcase\ so Windows and Linux evaluation stays strictly case-sensitive
  • Cleaned up the redundant subsumed checks and clarified the conservative matcher docstring
  • Added unit tests for negation overrides & case-sensitivity
  • Added the .changeset\ entry

Tests are green locally. Let me know if anything else is needed!

Appreciate it. There's two things to call out:

  1. Ruff is failing on several long lines in the tests (and thus failing the Lint CI check)
  2. One additional 'important' from the Cladue re-review.

If you want to take a stab at addressing those, go for it, otherwise I can fill in the gaps later. Don't worry about the test coverage percentage unless you wanted to tackle that as well, but it isn't nearly as important as the other items.

@amasen02

amasen02 commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor Author

Hey @schubydoo, thanks for following up! Just pushed an update covering both:

  • Fixed the line lengths in tests/test_config_write.py so ruff check passes cleanly.
  • Extended _rule_matches to support slash-containing wildcard and negation patterns (such as !.claude/*.json or !**/settings.local.json), ensuring path-level glob negations correctly un-ignore and append as expected.
  • Added unit tests for slash-containing negation overrides.

All tests and linter checks pass cleanly locally.

amasen02 and others added 4 commits September 4, 2026 08:37
…e covers path (schubydoo#1484)

- Add _is_gitignored_by_existing_rules to check if a path is already covered
- Support ancestor directory rules (/.claude, .claude/, .claude, a/b/)
- Support exact matches with or without leading slash and wildcard basenames (*.bak)
- Skip redundant appends in ensure_gitignored for target file and backup sibling
- Add tests covering covered no-ops, uncovered appends, and nested ancestor rules
`_is_gitignored_by_existing_rules` stripped git's leading-slash anchor and used
fnmatch, whose `*` crosses `/`, so a positive `/*.json` reported nested
`.claude/settings.local.json` as already ignored and skipped the append, leaving
the secret-bearing file git-tracked (schubydoo#1484 review). Trust a slash-wildcard only
for a negation, where a match just clears is_ignored and appends (the safe
direction). Cover the anchored-glob case, the positive over-append, the `**/`
negation sub-branch, and the empty/comment/non-matching edges.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@schubydoo
schubydoo force-pushed the fix/config-write-skip-redundant-gitignore-1484 branch from 228e68a to 3c6bd20 Compare September 4, 2026 15:37
@schubydoo

Copy link
Copy Markdown
Owner

@claude review

@claude

claude Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

Claude finished @schubydoo's task in 3m 50s —— View job


Re-review complete

  • Read the review instructions from the base branch
  • Read the new diff and the current config_write.py
  • Reconcile the prior Important finding (slash-containing negation)
  • Read the new tests
  • Submit one grouped review

Verdict: 1 important, 0 nits. The prior Important finding is fixed. One narrow residual case of the same class remains open.

See the review for the finding and the inline thread.
· branch

@claude claude 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.

1 important, 0 nits

Previous findings

  • 🔴 A negation pattern that contains a slash still leaves the file trackable — FIXED. _rule_matches now takes a negation flag, and lines 1598-1606 evaluate a slash-containing pattern with fnmatchcase for a negation only. Both forms from that finding now clear the flag and append: !.claude/*.json matches on the whole-path branch, and !**/settings.local.json matches on the **/ sub-pattern branch. test_ensure_gitignored_appends_when_slash_negation_overrides_wildcard and test_ensure_gitignored_appends_when_doublestar_negation_matches_root_file pin both.

The asymmetry the new comment describes is the right one, and it is worth recording: a negation match only clears the flag, so over-matching there causes an append. A positive match sets it, so over-matching there causes a skip. Restricting the glob branch to negations puts every inexact answer on the append side. test_ensure_gitignored_appends_when_root_anchored_glob_does_not_cover_nested and test_ensure_gitignored_over_appends_a_positive_slash_wildcard pin that choice, so a later edit cannot quietly reverse it.

All four Nits from the first review stayed fixed.

New findings

Finding Severity Location
A !a/**/b negation is not matched when **/ stands for zero directories, so the helper still answers "covered" for a file git tracks 🔴 Important src/clauster/config_write.py:1601

This is the last case of the class the previous two rounds closed, and the trigger is narrow. I raise it because the docstring at lines 1549-1552 states the opposite contract, and because the payload is the plaintext env map in settings.local.json and its .bak. The fix is one condition.

How I verified. By reading, against git's documented ** rule ("a slash followed by two consecutive asterisks then a slash matches zero or more directories") and the five call paths that depend on this function (config_write_settings.py:339, config_write_hooks.py:300, config_write_permissions.py:209, config_write_skills.py:703, claude_md.py:358). I traced every branch of _rule_matches by hand for the shapes the tests use and for the shapes they do not. I did not run the suite. That is the design here: CI is the measurement, across three operating systems.

Not flagged. The nine new tests read correctly, and the four that pin the over-append direction are the ones that make this function safe to edit later. The comment block at lines 1591-1597 explains the asymmetry rather than restating the code, which is the right thing to leave for the next reader. No docs/ page describes this behaviour, so no documentation update is due.

Comment thread src/clauster/config_write.py
@amasen02

amasen02 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the /**/ zero-directory negation finding in commit 75c1c1c:

  • In _rule_matches, when evaluating a negation rule containing a slash, if /**/ is present in clean_rule we evaluate whether norm_target matches clean_rule.replace("/**/", "/") (as well as for sub-patterns under leading **/). This correctly matches when /**/ stands for zero intermediate directories (e.g. !a/**/b matching a/b), clearing the ignored flag and guaranteeing append.
  • Added test_ensure_gitignored_appends_when_doublestar_negation_matches_zero_directories in tests/test_config_write.py.
  • Verified clean suite pass (178 passed) and ruff lint/format passing.

@schubydoo

Copy link
Copy Markdown
Owner

@claude review

@claude

claude Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

Claude finished @schubydoo's task in 2m 55s —— View job


Re-review complete

  • Read the review instructions from the base branch
  • Read the new diff and the current config_write.py
  • Reconcile the prior Important finding (/**/ zero directories)
  • Read the new tests
  • Submit one grouped review

Verdict: 0 important, 0 nits. The prior finding is fixed and no new Important finding remains.

See the review for the reconciliation and what I traced.
· branch

@claude claude 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.

0 important, 0 nits. No important findings.

Previous findings

  • 🔴 A !a/**/b negation with zero intermediate directories is not matched — FIXED. Lines 1601-1604 collapse /**/ to / and re-test with fnmatchcase, which is the condition the finding named. Trace !.claude/**/settings.local.json against .claude/settings.local.json: the whole-path test at line 1599 fails, the collapsed pattern .claude/settings.local.json matches, _rule_matches returns True, is_ignored goes back to False, and ensure_gitignored appends. The same collapse is applied to the **/-prefixed sub-pattern at lines 1610-1616, so !**/.claude/**/settings.local.json clears too. test_ensure_gitignored_appends_when_doublestar_negation_matches_zero_directories pins the case.

Every finding from the three earlier rounds stays fixed: the top-to-bottom negation scan, the slash-containing negation, fnmatchcase for case sensitivity, the docstring caveat, the subsumed conditions, and the changeset.

New findings

None. This closes the class the last three rounds worked through.

I re-traced each branch of _rule_matches against shapes the tests do not use, in both directions.

  • Fail-open is the direction that matters, because the payload is the plaintext env map in settings.local.json and its .bak. Only two branches give a file-level "covered": the exact match at line 1572 and the basename wildcard at line 1588. Both are now cleared by every negation shape I could build for these targets: !settings.local.json, !*.json, !.claude/*.json, !**/settings.local.json, !/.claude/settings.local.json, !.claude/**, !.claude/**/*.json, and the zero-directory !.claude/**/settings.local.json.
  • Fail-closed is untidy and safe. A positive .claude/* with no negation still appends, and test_ensure_gitignored_over_appends_a_positive_slash_wildcard records that as the intended direction.

The three ancestor-directory branches at lines 1576-1585 cannot be re-opened by a negation, because git does not re-include a file under an excluded directory. They remain the only branches that answer "covered" without a glob evaluation, which is what keeps the helper conservative.

How I verified. By reading, against git's documented ** rule ("a slash followed by two consecutive asterisks then a slash matches zero or more directories") and the six call paths that depend on this function (config_write_settings.py:339, config_write_hooks.py:300, config_write_permissions.py:209, config_write_skills.py:703, claude_md.py:358, app.py:3712). I did not run the suite. That is the design here: CI is the measurement, across three operating systems.

Not flagged. The new test reads correctly and exercises the branch the finding named, not a neighbouring one. No docs/ page mentions .gitignore, so no documentation update is due. Per the re-review rules, new Nits are suppressed after the first review, so this round reports Important findings only.

@schubydoo schubydoo left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@amasen02 Thank you again! Approving.

@schubydoo
schubydoo enabled auto-merge (squash) September 4, 2026 16:22
@schubydoo
schubydoo disabled auto-merge September 4, 2026 16:29
@schubydoo
schubydoo enabled auto-merge (squash) September 4, 2026 16:30
@schubydoo schubydoo closed this Sep 4, 2026
auto-merge was automatically disabled September 4, 2026 16:36

Pull request was closed

@schubydoo schubydoo reopened this Sep 4, 2026
@schubydoo
schubydoo enabled auto-merge (squash) September 4, 2026 16:36
@schubydoo
schubydoo merged commit 52e29bb into schubydoo:main Sep 4, 2026
70 checks passed
schubydoo pushed a commit that referenced this pull request Sep 5, 2026
Merging this PR tags and publishes **v1.2.0**.

Generated by knope from the `.changeset/` fragments — don't edit it by
hand; add or amend a changeset on the source PR instead.

---

## Features

- Bridge liveness now pairs the wall-clock start time with a
boot-relative one that NTP cannot move. A host clock correction no
longer marks a running bridge Stopped, blanks its CPU/RAM chip, or
deletes its card.
([#1400](#1400))
- `clauster keepers --kill <pid> --force` now stops a live keeper that
the normal cleanup spared on a still-carded project. It runs the same
PID-reuse identity check first, and is the recovery path for a keeper no
other automated stop can reach.
([#1458](#1458))

## Fixes

- When the host clock is stepped by any amount, a bridge started after
this release keeps its Running card. A bridge record left by a previous
boot is no longer mistaken for a live process on a recycled PID. A
bridge started before this release is unchanged until its next spawn.
([#1431](#1431))
- Stopping an Interactive Session now checks the keeper PID against the
keeper start time recorded in the instance row. It no longer trusts only
a snapshot taken as the stop begins. A PID recycled onto an unrelated
process before the stop ran is no longer force-killed.
([#1479](#1479))
- Keeper liveness now pairs the wall-clock start time with a
boot-relative one that NTP cannot move. A host clock correction no
longer lets Forget delete the record of a running Interactive Session.
([#1427](#1427))
- ensure_gitignored avoids appending redundant entries when covered by
existing .gitignore ancestor directory rules, exact matches, or basename
patterns, while respecting subsequent negation rules.
([#1485](#1485))
- On Windows, a broken ConPTY now ends the Interactive Session and marks
its bridge stopped. The card no longer reads live over a bridge that is
gone. ([#1416](#1416))
- Subagent and skill frontmatter now parse the same way. A `---` fence
line can carry trailing whitespace on both surfaces. Before, a skill's
opening fence with trailing whitespace was rejected. A skill body no
longer keeps the whitespace after its closing fence. Both surfaces now
reject a mismatched explicit tag (`!!int`, `!!float`, `!!bool`,
`!!timestamp`) as a 422 instead of a 500.
([#1367](#1367))
- A config file that exists but cannot be read now fails the doctor
config row plainly. The other CLI verbs now exit 2 with a `config error`
message instead of a traceback. Before, an unreadable file made
`clauster doctor` and `/api/doctor` answer a 500.
([#1447](#1447))
- Subagent or skill frontmatter that parses but holds a value the JSON
response cannot represent is now refused as a 422. The shapes are a
non-finite float (`.nan`/`.inf`), a non-UTF-8 `!!binary`, and an integer
too large to serialize. A string holding a lone surrogate is refused
too. Each returned a 500 before.
([#1448](#1448))
- A self-referential YAML alias in subagent or skill frontmatter is now
rejected as a 422 at write time. Before, the write was accepted and
reading the stored value back failed with a 500.
([#1394](#1394))
- When the host clock is corrected, a Direct Session started after this
release keeps its Kill and Resume buttons. Before, a clock correction
reported a running session as lost. Sessions started before it are
unchanged until their next spawn.
([#1425](#1425))
- A hosted session whose persisted record holds a wrong-typed value now
reattaches with default metadata and a logged warning. Before, it failed
clauster's startup for every hosted session.
([#1377](#1377))
- A hosted session whose saved conversation id is off-shape now keeps
that id on disk, so an operator can repair it by hand. The dashboard
hides Resume for such a session, whether the id was captured live or
reloaded, instead of offering a Resume that only errors.
([#1453](#1453))
- Two gaps a corrupt `hosted_state.json` opened are now closed. Two
records that share an instance id no longer let a cached client id
resolve to the wrong session. A session whose saved project is
unreadable now says why it cannot be resumed, instead of offering a
Resume that never works.
([#1397](#1397))
- An empty `claude_session_uuid` in a corrupt `hosted_state.json` record
no longer blocks a hosted session from learning its real session id.
Before, that row lost `--resume` for the life of the process.
([#1391](#1391))
- An Interactive Session whose bridge reached ready without capturing a
connect link now reads Running after a Clauster restart. It matches the
spawn path. Before, a ready sidecar with no link and no notice
reattached stuck at Starting for the life of the process.
([#1460](#1460))
- Winding down a lingering pty keeper now checks its boot-relative start
time, so a host clock correction mid-shutdown no longer leaves the
keeper running.
([#1409](#1409))
- Keeper sidecars and hosted-session rows now record a boot id. A host
clock step no longer makes a live Interactive Session keeper or hosted
agent look dead across a reboot. A keeper or hosted session started
before this release is unchanged until its next spawn.
([#1462](#1462))
- The standalone binary now loads a `clauster deps install pty` pyte in
the Interactive Session keeper, so the live terminal works without
setting `CLAUSTER_PYTE_PATH`.
([#1488](#1488))
- An Interactive Session that captured no connect link now keeps its
notice and its Running status across a Clauster restart. Before, the
reattached card dropped the notice and flipped to Starting.
([#1451](#1451))
- A pyte render fault during the PTY keeper's connect-URL scrape no
longer kills the keeper. Before, it stranded the discovery sidecar at
`starting` or `ready` with the bridge orphaned. The scrape now skips
that chunk and retries on the next one.
([#1388](#1388))
- An Interactive Session that captured no connect link now names the
reason on its card.
([#1426](#1426))
- Stopping an Interactive Session now winds its keeper down on a host
whose `/proc` reports no boot time. Before, the keeper and its bridge
kept running and nothing was logged.
([#1427](#1427))
- When a pywinpty liveness check faults during teardown on Windows, a
`claude` login flow no longer stays stuck `active`. The four ConPTY
`isalive()` calls now read a fault as dead, and the teardown always
clears the flow.
([#1454](#1454))
- A terminal frame that pyte cannot render no longer crashes the
dashboard login mid-flow. The authorize-URL and token scans skip that
frame and keep scanning. If the sign-in still fails, the fault is named.
([#1375](#1375))
- The dashboard metrics sampler now handles a host with no procfs
`btime` line. It reports the bridge's own CPU and memory instead of
failing the request.
([#1430](#1430))
- An unterminated OSC 8 (terminal hyperlink) escape no longer swallows
the following line into its URI. Before, on the Windows ConPTY login
path, that handed the operator a corrupted, unopenable `claude
setup-token` authorize URL. On that path the hyperlink target is the
only recoverable copy.
([#1385](#1385))
- A hostile `bridge-pointer.json` now follows the documented contract
that a malformed pointer degrades to None. Deeply-nested JSON and an
oversized integer literal no longer raise out of pointer loading.
`clauster` can now back up and remove such a pointer, as it already did
for other corrupt ones. Before, the pointer wedged the next bridge
start. ([#1383](#1383))
- Reading a settings.json that holds a huge integer or a NaN/Infinity
float literal now returns a 422, not a 500. The message names only the
class of the offending value, never the value.
([#1463](#1463))
- A corrupt legacy `state.json` or `hosted_state.json` now degrades to
an empty store, as documented. It no longer takes clauster down on the
boot that imports it. The import logs every corrupt shape and copies the
file aside once as `*.corrupt.bak`. `clauster migrate` refuses to
rewrite a file it cannot read and leaves it untouched.
([#1413](#1413))
- Seven untrusted-input parsers now skip or reject a deeply-nested or
non-object payload instead of raising, and a junk `daemon_last_seq` in
`hosted_state.json` no longer blocks startup.
([#1336](#1336))
- Two more transcript readers skip a deeply-nested line instead of
failing the usage tally and the transcript viewer with a 500.
([#1372](#1372))

## Security

- `doctor`'s `config` row now reports a broken `clauster.yml` by
position and key path only. It never echoes the offending YAML line, the
rejected scalar, or the parsed mapping.
([#1428](#1428))
- A secret inside a YAML `!!omap`, `!!pairs`, or `!!set` in subagent or
skill frontmatter is now masked in the displayed frontmatter. An alias
pyramid that expands exponentially (a billion-laughs attack) is now
refused as a 422. Before, it tied up a worker on every read.
([#1414](#1414))
- A subagent's frontmatter now masks a secret written as a `!!binary`
value. It masks the base64 spelling like the plain-string spelling, by
key name and by secret-shaped content.
([#1459](#1459))
- A YAML `<<` merge-key pyramid in subagent or skill frontmatter is now
refused as a 422. Before, it tied up a worker thread for seconds inside
the parse on every read that lists agents or skills.
([#1461](#1461))
- A YAML parse error no longer echoes the offending frontmatter token to
the dashboard. A rejected subagent or skill now reports the error
category with its line and column. Before, PyYAML's message quoted the
token itself for an undefined alias, a duplicate anchor, or an unknown
tag. ([#1396](#1396))
- A hosted session's stored conversation id is now shape-checked before
it becomes the `--resume` value. A tampered record can no longer add an
argument to the spawn command line. An ended session that cannot be
resumed now says, on its row and in its panel, which part of its record
is missing. ([#1412](#1412))
- A stray OSC 8 (terminal hyperlink) opener can no longer swallow the
real authorize URL, because its parameter run now excludes ESC. The live
pty screen also re-redacts each row after the width re-fit, so
truncation can no longer shear a redacted identifier back into view.
([#1371](#1371))
- The live pty-screen view now masks a redactable token that wraps
across the fixed-width row boundary, which per-row redaction missed.
([#1492](#1492))
- The live terminal-screen view now masks a real identifier a consumed
escape welded onto the word before it. It also masks an id welded to
another id, and it matches the real id shape, so an ordinary name like
`resolve_session_transcript` stays readable.
([#1442](#1442))
- A greedy secret or id token can weld onto a UUID with no separator and
eat its leading hex digits. On the live pty-screen view, the rest of the
UUID no longer reaches the browser.
([#1499](#1499))
- A zero-width or invisible Unicode character inside a session or
environment identifier no longer hides it from the log redaction. Such
characters are now removed from the redacted log the dashboard shows.
([#1441](#1441))
- Log redaction now masks against the view a browser renders. An escape
sequence or an invisible control character can no longer weld a session
identifier onto the word before it, or split one in two. DCS, SOS, PM,
and APC escape payloads (device-control and private-message strings) are
now stripped whole, not streamed as readable junk.
([#1432](#1432))
- OSC escape sequences (terminal title, hyperlink, clipboard) are
stripped whole from the streamed bridge log and the `claude login` URL
scan. Before, their payload showed as visible text.
([#1337](#1337))
- The config editor documents which surfaces mask secret-shaped values
and which return raw, and recommends against storing a secret in a
subagent file.
([#1490](#1490))
- A rendered transcript turn's `timestamp` is now redacted like its
other fields, so an identifier carried in that field no longer reaches
the browser verbatim.
([#1372](#1372))

## Build System & Dependencies

- The unused `watchdog` runtime dependency is dropped, so installs, the
container image, and the frozen binary no longer ship it.
([#1325](#1325))
- Clauster now requires psutil 7.1 or newer. On a host whose `/proc`
reports no boot time, earlier psutil versions raised in the process
constructor. ([#1427](#1427))

Co-authored-by: clauster-ci[bot] <289303168+clauster-ci[bot]@users.noreply.github.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.

config-write: skip a redundant .gitignore append when an ancestor rule already covers the file

2 participants