fix(claude-memory,session-flow): quote-aware topic-docs seam parser via shared helper#756
Conversation
…ia shared helper
The topic-docs `memory_dir` seam was parsed inline at two sites with a naive
`${val%%#*}`-first strip that truncated a legitimately quoted value containing
`#` (`"a#b"` -> `a`) and stripped comments before resolving quotes. Extract a
single shared `parse-concern-value.sh` helper (quote-resolution first, then
whitespace-anchored comment strip, trim, trailing-slash normalize) materialized
from `lib/` into both consuming plugins, and route the claude-memory
orphan-rule-check and the session-flow retro `HANDOFF_DIR` snippet through it.
The helper takes the caller-resolved rung-2 location (a save-point convention
declared in CLAUDE.md / .claude/rules) as an input argument, so an absent
concern file no longer collapses straight to `.work`: prose stays an inference
source the agent resolves, not a machine key the script parses.
A dedicated scripts/sync-parse-concern-value.sh (--check / --check-bump) plus a
parse-concern-value-sync CI gate keep the plugin copies byte-identical to the
source, mirroring the hook-utils sync pattern.
Co-Authored-By: Claude Opus <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 17s —— View job PR Review — fix(claude-memory,session-flow): quote-aware topic-docs seam parser via shared helper
SummaryThe fix is correct and well-structured. The root bug (naive Two findings below worth addressing before merge, plus several minor notes. Finding 1 —
|
|
Claude finished @kyle-sexton's task in 3m 16s —— View job Security Review — PR #756
Scope: all 13 changed files reviewed. No critical or important security findings. One suggestion documented below. FindingsSUGGESTION — Unvalidated
|
| Area | Result |
|---|---|
CI workflow trigger (push + pull_request, not pull_request_target) |
Safe — fork PRs cannot access secrets |
github.base_ref in new CI job (line 270) |
Safe — isolated to env:, never directly interpolated in run: block; consistent with all prior BASE_REF usages in the file |
| Workflow permissions | Safe — contents: read at workflow level, persist-credentials: false on all checkouts, no token usage in new job |
$SCRIPT_DIR resolution and child-script execution in orphan-rule-check.sh |
Safe — resolved from BASH_SOURCE[0], not user-controlled input |
git grep pathspec with $memory_dir interpolation (orphan-rule-check.sh:91) |
Safe — the entire pathspec is a single double-quoted argument; malformed content produces a git error, not code execution |
File path handling (concern_file arg) |
Safe — read-only (-f check + sed), no write path; no privilege escalation beyond existing repo access |
scripts/sync-parse-concern-value.sh --check-bump (git show "$base:$manifest") |
Safe — $base is a git ref argument used as a git argument, not interpolated into shell |
| Action pins in new CI job | Safe — uses same SHA-pinned actions/checkout and actions/setup-node as all other jobs |
No objections to merging on security grounds. The suggestion above is optional hardening for a latent API-contract concern, not a blocker.
Co-Authored-By: Claude Opus <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 51s —— View job PR Review — fix(claude-memory,session-flow): quote-aware topic-docs seam parser via shared helper
Latest commit (
|
| Area | Verdict |
|---|---|
Parse order in strip_value (quote → comment → trim → slash) |
Correct |
# adjacent to non-space preserved in unquoted path (.work/#topic) |
Correct — [[:space:]]#.*$ pattern requires leading space |
| Absent concern file degrades to fallback, then empty | Correct — -f check gates the sed path |
orphan-rule-check.sh delegation (line 68–69) |
Correct — no fallback passed; caller owns the .work default |
session.md two-step resolution (DECLARED_SAVEPOINT:- → MEMORY_DIR:-.work) |
Correct — matches helper's resolution contract |
| Fallback passthrough not normalized (by design) | Documented in test line 77; callers must pass clean values |
CI job wired into ci-status needs: (line 613) |
Confirmed |
fetch-depth: 0 on parse-concern-value-sync for base-ref resolution |
Correct |
github.base_ref in env:, not interpolated into run: |
Safe |
jq dependency in --check-bump |
Acceptable — available on ubuntu-24.04, consistent with hook-utils precedent |
Regression test Case 9 (quoted ".scratch#dir" preserved) |
Directly pins the reported bug |
Test coverage
19 unit tests in lib/parse-concern-value.test.sh and 14 integration tests in orphan-rule-check.test.sh cover the regression, the held behaviors, fallback resolution, and the absent-file path. Coverage is well-targeted and sufficient for the scope of this fix.
|
Claude finished @kyle-sexton's task in 2m 39s —— View job Security Review — PR #756
Scope: all 13 changed files reviewed. No critical or important security findings. One suggestion carried forward from the prior automated security review, confirmed here. Finding —
|
| Area | Result |
|---|---|
Workflow trigger (push + pull_request, not pull_request_target) |
Safe — fork PRs cannot access secrets |
github.base_ref usage in new parse-concern-value-sync CI job (line 270) |
Safe — isolated to env:, never directly interpolated in a run: block; matches the pattern used by every other bump-gate job |
Workflow permissions (contents: read at workflow level) |
Safe — no token widening introduced |
persist-credentials: false on all checkouts in new job |
Safe — matches repo convention |
$SCRIPT_DIR resolution in orphan-rule-check.sh |
Safe — derived from BASH_SOURCE[0], not user-controlled input |
concern_file path (${repo_root}/.claude/topic-docs.yaml) |
Safe — constructed from git rev-parse --show-toplevel, a trusted source; sed reads only, no write path |
$memory_dir in git grep pathspec (orphan-rule-check.sh:91) |
Benign — advisory-only check (exits 0 always); pathspec is behind -- so no option injection; worst case is incorrect file exclusion, not privilege escalation |
git show "$base:$manifest" in sync-parse-concern-value.sh (line 57) |
Safe — $base comes from origin/$BASE_REF where BASE_REF is set in env: from github.base_ref, not inline in run:; branch names are constrained by git |
jq usage for version parsing in --check-bump |
Safe — jq does not execute shell commands from its input |
| Action pins in new CI job | Safe — actions/checkout pinned to same SHA as all other jobs in the file |
ci-status aggregate gates on parse-concern-value-sync (line 613) |
Safe — new lane correctly wired into required-check aggregate |
No objections to merging on security grounds. The suggestion above is optional hardening for a latent API-contract concern already flagged by the prior automated review — not a blocker.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 52ed9448e8
ℹ️ 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".
…am-parsing # Conflicts: # plugins/claude-memory/CHANGELOG.md
|
Claude finished @kyle-sexton's task in 3m 12s —— View job Security Review — PR #756
Scope: all 13 changed files reviewed against Finding —
|
| Area | Result |
|---|---|
Workflow trigger (push + pull_request, not pull_request_target) |
Safe — fork PRs cannot access secrets |
github.base_ref in new parse-concern-value-sync job |
Safe — isolated to env: as BASE_REF, never directly interpolated in a run: block; matches the pattern used by every other bump-gate job |
Workflow permissions (contents: read at top level, no widening in new job) |
Safe — no new permission grants introduced |
persist-credentials: false on checkout in new job |
Safe — matches repo convention |
$SCRIPT_DIR resolution in orphan-rule-check.sh |
Safe — derived from BASH_SOURCE[0], not user input |
$concern_file path construction (${repo_root}/.claude/topic-docs.yaml) |
Safe — repo_root from git rev-parse --show-toplevel, a trusted source; sed reads only, no write or exec path |
$(concern_file) content used in sed output — no eval, source, or exec sink |
Safe — strip_value only applies string operations to the extracted line; file content cannot cause code execution |
$memory_dir in git grep pathspec (orphan-rule-check.sh:91) |
Benign — script is advisory-only (always exits 0); pathspec is behind --, so injection would produce git errors or incorrect exclusions, not privilege escalation |
git show "$base:$manifest" in sync-parse-concern-value.sh --check-bump |
Safe — $base is origin/$BASE_REF where BASE_REF comes from env:, not inline run: interpolation; passed as a git argument, not to a shell |
jq usage for version parsing |
Safe — jq does not execute shell commands from its input |
| Action pins in new CI job | Safe — actions/checkout pinned to same SHA (9c091bb2...) as all other jobs in the file |
ci-status aggregate gates on parse-concern-value-sync (line 613) |
Correct — new lane is fully wired into the required-check aggregate |
No objections to merging on security grounds. The suggestion above is optional hardening for a latent API-contract concern — not a blocker. Three prior automated security reviews reached the same conclusion.
|
Claude finished @kyle-sexton's task in 1m 59s —— View job PR Review — fix(claude-memory,session-flow): quote-aware topic-docs seam parser via shared helper
Latest commitsThe branch now has three commits: the substantive fix ( Status of open findingsTwo findings from prior review cycles remain open in the current HEAD ( Finding 1 —
|
| Area | Verdict |
|---|---|
Root bug: quote-resolution before comment-strip in strip_value |
Fixed correctly |
# adjacent to non-space preserved in unquoted path (.work/#topic) |
Correct — [[:space:]]#.*$ requires leading space |
| Absent concern file degrades to fallback, then empty | Correct — -f check gates the sed path |
orphan-rule-check.sh delegation |
Correct — caller owns the .work default |
session.md two-step default (DECLARED_SAVEPOINT:- → MEMORY_DIR:-.work) |
Correct resolution order |
Regression test Case 9 (quoted ".scratch#dir" preserved) |
Directly pins reported bug |
CI parse-concern-value-sync job wired into ci-status |
Confirmed |
Merge commit (563e540) touches none of the 13 changed files |
Confirmed — upstream sync only |
Overall verdict
The core fix is correct, well-tested, and structurally sound. All three open items are forward-looking quality concerns rather than bugs in the current fix:
- Finding 1 (key guard) — optional hardening, zero live risk today, high value before claude-memory/session-flow/docs-hygiene: memory_dir/contract_dir config-value parsing is hand-rolled and copy-pasted across ≥4 files, not a shared helper #474 adds consumers
- Finding 2 (manifest comment) — one-line documentation gap with no functional impact
- Codex finding (
DECLARED_SAVEPOINTclarity) — low-urgency wording improvement
The PR is ready to merge as-is if the team is comfortable deferring these to a follow-up. Addressing Finding 1 before adding new consumers (the planned #474 work) would be the natural forcing function.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 563e540cfd
ℹ️ 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".
…allback A comment-only YAML value (`memory_dir: # use default`, YAML-null) was taken literally by parse-concern-value.sh: the extraction strips the post-colon whitespace, so `raw` begins with `#`, and the unquoted comment strip only fired on a whitespace-*preceded* `#`. The value resolved to `# use default` instead of falling through to the caller fallback. Impact was fail-open in a check: orphan-rule-check stopped excluding the default `.work/` tier and could let a `.work` reference mask a real orphan; retro's handoff-chain search resolved a bogus `# use default/handoffs`. Treat a `#` that starts the unquoted value as a comment (YAML-null) so resolution degrades to the fallback / documented default. Synced to both plugin copies; added red-then-green regression cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HZqwoLTHPpPRzLpRGzbKRq
|
Claude finished @kyle-sexton's task in 2m 20s —— View job PR Review — fix(claude-memory,session-flow): quote-aware topic-docs seam parser via shared helper
Latest commit (
|
| Area | Verdict |
|---|---|
Comment-only value (memory_dir: # use default) now falls to fallback |
Fixed in ba183d5 — s/^#.*$// applied before space-preceded strip |
Quoted a#b and .work/#topic still preserve # |
Correct — neither new pattern touches non-leading, non-space-preceded # |
strip_value parse order (quote → comment → trim → slash) |
Correct and unchanged |
Both plugin copies byte-identical to lib/parse-concern-value.sh |
Verified (diff clean) |
Plugin versions bumped (claude-memory → 0.2.3, session-flow → 0.10.4) |
Confirmed |
| Regression tests added for comment-only behavior (lines 76–79) | Confirmed; correctly pin the new behavior |
ci-status gates on parse-concern-value-sync |
Confirmed (CI job wired in; prior review verified) |
Overall verdict
The core fix (quote-aware comment strip) is correct, the Codex P2 regression (comment-only scalar) is correctly addressed in ba183d5, and all sync/drift/CI machinery is in order. The three remaining open findings are forward-looking quality concerns — not bugs in the current code:
- Finding 1 (key guard) is zero-risk today; becomes the natural claude-memory/session-flow/docs-hygiene: memory_dir/contract_dir config-value parsing is hand-rolled and copy-pasted across ≥4 files, not a shared helper #474 forcing function
- Finding 2 (manifest comment) is one documentation line; no functional impact
- Finding 3 (
DECLARED_SAVEPOINTclarity) is a wording gap in agent-facing prose
The PR is mergeable as-is. Addressing Finding 1 before new consumers land in #474 remains the recommended path.
|
Claude finished @kyle-sexton's task in 2m 41s —— View job Security Review — PR #756
Scope: all 13 changed files reviewed, including the latest commit ( Finding —
|
| Area | Result |
|---|---|
Workflow trigger (push + pull_request, not pull_request_target) |
Safe — fork PRs cannot access secrets |
github.base_ref in parse-concern-value-sync job |
Safe — isolated to env: as BASE_REF, never directly interpolated in a run: block; matches the pattern used by every other bump-gate job |
Workflow permissions (contents: read at top level, no widening in new job) |
Safe |
persist-credentials: false on checkout in new job |
Safe — matches repo convention |
$SCRIPT_DIR resolution in orphan-rule-check.sh |
Safe — derived from BASH_SOURCE[0], not user-controlled input |
concern_file path construction (${repo_root}/.claude/topic-docs.yaml) |
Safe — repo_root from git rev-parse --show-toplevel, a trusted source; read-only sed, no write or exec path |
strip_value() sed subshell on line 94 (ba183d5 two-expression form) |
Safe — printf '%s' "$val" pipes to sed with no e flag or exec sink; file content cannot cause code execution |
$memory_dir in git grep pathspec (orphan-rule-check.sh:91) |
Benign — pathspec behind --, script always exits 0; worst case is wrong exclusion, not privilege escalation |
git show "$base:$manifest" in sync-parse-concern-value.sh --check-bump (line 57) |
Safe — $base is origin/$BASE_REF where BASE_REF comes from env:, not inline run: interpolation |
jq usage for version parsing (line 57) |
Safe — jq does not execute shell commands from its input |
| Action pins in new CI job | Safe — actions/checkout@9c091bb2... pinned to same SHA as all other jobs |
ci-status aggregate gates on parse-concern-value-sync (line 613) |
Correct — new lane fully wired into the required-check aggregate |
No objections to merging on security grounds. The suggestion above is optional hardening for a latent API-contract concern — not a blocker. Four prior automated security reviews reached the same conclusion.
…o shared parse-concern-value helper (#847) ## Summary Final wiring of the ratified bounded-widen scope on #474: fold the last two verified hand-rolled `memory_dir`/`contract_dir` copies into #482's already-landed `parse-concern-value.sh` helper (merged via #756). - **`plugins/docs-hygiene/skills/audit-noise/scripts/lib/noise-shapes.sh`** — `audit_noise_convention_roots_pattern` hand-rolled the same naive `${val%%#*}`-first strip #482 fixed at its two hot sites (truncates a quoted value at an interior `#`), plus a defect unique to this copy: `${val//[[:space:]]/}` collapsed *all* interior whitespace, and there was no quote-stripping. Now resolves both keys through the shared helper, materialized to `plugins/docs-hygiene/skills/audit-noise/scripts/lib/parse-concern-value.sh` (byte-identical + mode-identical, `100755`, to the two existing copies) and registered in `scripts/sync-parse-concern-value.sh`'s `copies` array — the same upstream-SSOT + sync-materialization pattern as the two prior sites (installed plugins are cache-isolated and cannot `source` a sibling's file). - **`plugins/session-flow/skills/handoff/context/structure.md`** L116 — replaced the placeholder note (`# the concern file's memory_dir when set — resolve it first`, no mechanism named) with a pointer to the shared helper via the retro skill's Phase 1.1 as the worked call form. Doc note only: the handoff skill has no script of its own to rewire. Per the operator's ratified bounded-widen decision (2026-07-19T23:26Z) and the tower's 2026-07-21 decision comment on #474, the broader tracker-seam-resolution class (#470/#469/#432/#365/#369) stays explicitly OUT of scope. Version bumps stack past two independently-merged sibling PRs that landed while this one was open: docs-hygiene `0.8.0` → `0.8.2` (stacks past #846's `0.8.1`, an exact version collision resolved by re-numbering this PR's entry on top and preserving #846's CHANGELOG entry underneath); session-flow `0.11.0` → `0.12.1` (stacks past a merged `0.12.0` that added the `orient` skill). ## Related - #482 / #756 — landed the shared `parse-concern-value.sh` helper and its first two consumers; this PR extends it to the remaining two per the ratified bounded-widen scope. - #846 — merged concurrently with this PR and independently bumped `docs-hygiene` to `0.8.1`; this PR's version was restacked to `0.8.2` on top, both CHANGELOG entries preserved. - #470 / #469 / #432 / #365 / #369 — the broader tracker-seam-resolution class; explicitly excluded from this PR's scope per the operator's ratified decision. ## Test plan - [x] `bash plugins/docs-hygiene/skills/audit-noise/scripts/detect.test.sh` — 34/34 pass, including a new regression case: a quoted `memory_dir: ".scratch#dir/" # trailing comment` now correctly resolves to `.scratch#dir` (flags `.scratch#dir/foo/` as a concrete slice) instead of the old parser's silent truncation. - [x] Manual before/after demonstration of the two defects the old parser had: - Quoted + interior `#` + trailing comment + trailing slash: old `[.scratch]` (wrong — truncated inside the quotes) vs new `[.scratch#dir]` (correct). - Unquoted value with interior whitespace: old `[mydir]` (wrong — collapsed) vs new `[my dir]` (correct, surrounding whitespace trimmed only). - [x] `scripts/sync-parse-concern-value.sh --check` — all 3 plugin copies match `lib/parse-concern-value.sh`. - [x] `scripts/check-cross-plugin-source-drift.sh --check` — no unregistered/drifted clusters (the three copies have distinct within-plugin relpaths, so the generic drift check doesn't need to cluster them; the dedicated sync gate already covers them). - [x] `scripts/check-changelog-parity.sh --check` and `--check-bump origin/main` — both touched plugins (`docs-hygiene` 0.8.0→0.8.2, `session-flow` 0.11.0→0.12.1) carry matching CHANGELOG entries. - [x] `shellcheck` + `shfmt -d` — clean on all new/edited scripts. - [x] `scripts/validate-plugins.sh` — all manifests + catalog valid. Closes #474 --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Summary
The topic-docs
memory_dirseam was parsed inline at two sites with a naive${val%%#*}-first strip — the same naive-string-op class the #469/#470 whitespace fixes closed, left as a review-deferred residual. It truncated a legitimately quoted value containing#(memory_dir: "a#b"→a) because it stripped a "comment" before resolving surrounding quotes, and an absent concern file collapsed straight to.workwithout consulting the contract's rung-2 (a save-point convention declared inCLAUDE.md/.claude/rules).Per the issue's acceptance criteria, both sites now delegate to one shared parser rather than re-forking the duplicated block.
Fix
lib/parse-concern-value.sh(new) — the single seam parser. Order is quote-resolution first, then a quote-aware comment strip (unquoted#…only;a#b/.work/#topicpreserved), then whitespace-trim, then trailing-slash-normalize. Takes<concern-file> <key> [fallback]; thefallbackis the caller-resolved rung-2 location (prose is an inference source, not a machine key — the agent resolves it and passes it in). Emits empty only when nothing resolves, so the caller owns the documented.workdefault.orphan-rule-check.sh(real script) and the session-flow retroHANDOFF_DIRsnippet (retro/context/session.md). The non-interactive orphan check degrades to.workand notes that inferred/interactive resolution is the calling skill's job, rather than silently collapsing rung 2.lib/into each plugin (installed plugins are cache-isolated, so each must be self-contained — thelib/hook-utils.shprecedent). Newscripts/sync-parse-concern-value.sh(--check/--check-bump) + aparse-concern-value-syncCI job keep the copies byte-identical and enforce version bumps when the lib changes. The two copies live at different relpaths, so the generic cross-plugin drift check does not cluster them — a dedicated gate is required.Verification
All run locally (Git Bash):
lib/parse-concern-value.test.sh— 19/19 pass, incl. regressions: double/single-quoteda#band.work/#topickeep#; quoted value with trailing comment drops the comment but keeps the inner#; absent/empty key returns the supplied fallback; held behavior (unquoted trailing# commentstripped, whitespace trimmed, interior quoted space preserved, trailing slash normalized).orphan-rule-check.test.sh— 14/14 pass, incl. new Case 9: a ref inside a quoted".scratch#dir"tier is correctly excluded (the old%%#*truncated to.scratch, masking the orphan).scripts/sync-parse-concern-value.sh --check— copies match source.scripts/check-cross-plugin-source-drift.sh --check— no unregistered/drifted clusters.shellcheck+shfmt -d— clean on all new/edited scripts.scripts/validate-plugins.sh— all manifests + catalog valid.Closes #482
Related
Realizes the shared-helper shape #474 called for, scoped to #482's two sites. #474 (wiring the remaining consumers to this helper) stays parked and is unblocked by this PR.