Skip to content

fix(skills): harden remember/recall wrappers against downstream Qodo findings (0.9.3)#22

Merged
OriNachum merged 2 commits into
mainfrom
fix/remember-recall-wrapper-hardening
Jun 23, 2026
Merged

fix(skills): harden remember/recall wrappers against downstream Qodo findings (0.9.3)#22
OriNachum merged 2 commits into
mainfrom
fix/remember-recall-wrapper-hardening

Conversation

@OriNachum

Copy link
Copy Markdown
Contributor

What

The remember/recall skill wrappers fan out byte-verbatim (via rollout-cli's eidetic-memory recipe) into 57 AgentCulture repos. Qodo's 215 findings across those PRs collapse to a handful of root causes — and because the copies are byte-identical, the only durable fix is upstream here; a fix anywhere downstream is overwritten on the next rollout (~57× leverage).

Triaged via /think → converged spec at docs/specs/2026-06-23-…wrappers-are-h.md (frame under .devague/). Eight fixes, confined to the two wrapper scripts + their SKILL.md — no eidetic Python, schema, ranking, or storage touched.

Fix Root cause (verified) Change
FIX-1 / FIX-7 resolve_eidetic's uv-checkout fallback can never fire in a vendored copy, yet the hint implied it could Keep the dev fallback only for eidetic-cli's own repo; not-found prints one honest single-line hint:
FIX-2 remember.sh blocks forever on a no-arg interactive invocation (no guard; recall.sh had one) Guard on [ -t 0 ] → print usage + exit; piped NDJSON batch path unchanged
FIX-3 ~/.eidetic/memory ×5 in SKILL.md trips downstream cicd portability-lint (~/\.[A-Za-z], no carve-out) Reword → $HOME/.eidetic/memory (also fixes the loaded skill descriptions)
FIX-4 --help said "Public data only." but the wrapper defaults to --visibility private (stale since the private-default flip) State the private-by-default behavior + how --visibility public overrides
FIX-5 Empty suffix → silent fallback to the public default scope (privacy downgrade) One-line stderr warning; stdout stays clean for --json
FIX-6 recall.sh "help" printed usage instead of searching; empty query exited 0 help is a query; only -h/--help = usage; empty query = hint: + non-zero
FIX-8 resolve_scope suffix parse could SIGPIPE under set -o pipefail || true guard

Deferred (parked, non-blocking): parent-dir traversal guard, SKILL.md line length.

Out of scope → rollout-cli (recipe PR, not this repo): recipe not updating the consumer provenance ledger / skill-sources.md / README skill-count, uv.lock refresh, CHANGELOG ordering, delta-check note.

Won't-fix false positives: exit-codes-0/1/2 (conflicts with eidetic's own 0/1/2/3+ contract + intentional exec passthrough), --apply/dry-run (ingest isn't destructive), "requires external CLI" (the subprocess boundary is the design).

Verification

  • shellcheck clean on both scripts; bash -n OK
  • pty test proves no-arg remember.sh on a real TTY exits cleanly (no hang); piped batch path bypasses the guard
  • recall.sh "help" → searches (no usage); recall.sh ""hint: + exit 1; --help → usage + exit 0
  • FIX-5 warning fires only when no culture.yaml resolves (silent inside this repo)
  • grep -nE '~/\.[A-Za-z]' over both SKILL.md → clean (portability-lint passes)
  • teken cli doctor . --strict rubric → PASS; pytest -n auto304 passed, 2 skipped
  • uv.lock relocked to 0.9.3 (also cleared pre-existing 0.9.0 drift)

Follow-up

After merge: re-sync the rollout-cli eidetic-memory recipe (re-vendor the corrected bytes) and re-run the fan-out to refresh the 57 open PRs.

🤖 Generated with Claude Code

  • eidetic-cli (Claude)

…findings (0.9.3)

The remember/recall skill wrappers fan out byte-verbatim (via rollout-cli's
eidetic-memory recipe) into 57 AgentCulture repos. Qodo's 215 findings across
those PRs collapse to a handful of root causes that can only be fixed upstream
here — a fix anywhere else is overwritten on the next rollout. Eight fixes,
confined to the two wrapper scripts + their SKILL.md (no Python/schema/storage):

- FIX-1/7 resolve_eidetic: drop the unreachable uv-checkout fallback claim in
  vendored copies; not-found prints one honest single-line `hint:`.
- FIX-2 remember.sh: guard the no-arg interactive invocation ([ -t 0 ]) so it
  prints usage and exits instead of blocking on stdin; batch NDJSON unchanged.
- FIX-3 SKILL.md + descriptions: $HOME/.eidetic/memory (was ~/.eidetic/memory)
  so downstream cicd portability-lint stops failing.
- FIX-4 remember.sh usage: replace stale "Public data only." with the actual
  private-by-default behavior.
- FIX-5 scope resolution: warn on stderr instead of silently downgrading an
  expected-private record to the public default scope.
- FIX-6 recall.sh: treat bareword `help` as a query; empty query is now a
  hint:+non-zero error, not exit 0.
- FIX-8 resolve_scope: `|| true` guards the suffix parse against pipefail SIGPIPE.

Deferred (parked): parent-dir traversal guard, SKILL.md line length. Rollout-recipe
issues (provenance ledger / uv.lock / CHANGELOG) are out of scope here and belong
to rollout-cli. Spec + frame from /think included under docs/specs and .devague.

Verified: shellcheck clean; pty test proves no TTY hang; rubric green;
304 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VH4e7v8VZNcKHy4tzrJUVm
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Harden remember/recall skill wrappers for downstream vendoring (0.9.3)
🐞 Bug fix 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

Description

• Prevent wrapper hangs and clarify CLI-not-found remediation in vendored copies.
• Make scope/visibility defaults explicit and warn on unsafe public-scope fallback.
• Fix downstream portability-lint by replacing ~ paths with $HOME in docs.
Diagram

graph TD
A["Claude / Colleague"] --> B["remember.sh / recall.sh"] --> D{{"eidetic CLI"}} --> E[("$HOME/.eidetic/memory")]
B --> C[/"culture.yaml"/] --> B
subgraph Legend
direction LR
_agent["Agent"] ~~~ _wrap["Wrapper script"] ~~~ _cfg[/"Config file"/] ~~~ _ext{{"External CLI"}} ~~~ _db[("Store")]
end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Patch at vendoring time (rollout recipe)
  • ➕ Could avoid touching upstream wrappers for downstream-only lint quirks
  • ➕ Enables per-consumer customization without upstream contract changes
  • ➖ Breaks byte-verbatim provenance and increases drift risk
  • ➖ Still requires ongoing maintenance across recipe versions and rollouts
2. Relax downstream portability-lint rules for `~/.eidetic/...`
  • ➕ Minimal change to wrappers; fixes only the specific lint failure
  • ➕ Keeps wrapper docs unchanged
  • ➖ Requires coordinated changes across many downstream CI setups
  • ➖ Encourages more home-tilde patterns that hurt portability elsewhere
3. Rewrite wrappers as a small Python entrypoint
  • ➕ More robust parsing and richer error handling
  • ➕ Easier unit testing than bash
  • ➖ Heavier runtime dependency footprint in all 57 vendored repos
  • ➖ Higher change risk for a layer intended to be a thin passthrough

Recommendation: Keep the upstream, byte-identical wrapper hardening approach (as in this PR). Because these scripts are vendored verbatim into many repos, upstream fixes provide the highest leverage and avoid recipe-time patch drift. Follow up by re-syncing the rollout recipe to re-vendor these exact bytes.

Files changed (9) +484 / -25

Bug fix (2) +51 / -18
recall.shFix help/query semantics and harden scope resolution warnings +23/-9

Fix help/query semantics and harden scope resolution warnings

• Makes 'help' a valid query (only '-h/--help' shows usage) and treats empty query as an error with a hint. Hardens culture.yaml suffix parsing under 'set -o pipefail', emits a warning when no suffix resolves, and simplifies eidetic-not-found output to a single actionable hint.

.claude/skills/recall/scripts/recall.sh

remember.shPrevent interactive no-arg hang and align help with private defaults +28/-9

Prevent interactive no-arg hang and align help with private defaults

• Adds a TTY/no-arg guard to print usage instead of blocking on stdin, keeping piped NDJSON behavior unchanged. Updates help text to reflect private-by-default scope/visibility, warns on missing culture.yaml suffix (public fallback), hardens suffix parsing under pipefail, and simplifies eidetic-not-found hinting for vendored use.

.claude/skills/remember/scripts/remember.sh

Documentation (6) +432 / -6
SKILL.mdReplace '~' store path with '$HOME' for portability-lint +3/-3

Replace '~' store path with '$HOME' for portability-lint

• Updates documentation references from '~/.eidetic/memory' to '$HOME/.eidetic/memory' to satisfy downstream portability linting while preserving meaning.

.claude/skills/recall/SKILL.md

SKILL.mdReplace '~' store path with '$HOME' for portability-lint +2/-2

Replace '~' store path with '$HOME' for portability-lint

• Rewords the documented store path to '$HOME/.eidetic/memory' to avoid downstream CI portability violations without changing runtime behavior.

.claude/skills/remember/SKILL.md

currentPoint devague 'current' to the new wrapper-hardening frame +1/-1

Point devague 'current' to the new wrapper-hardening frame

• Updates the active devague frame slug to match the current work item for wrapper hardening.

.devague/current

eidetic-cli-s-remember-recall-skill-wrappers-are-h.jsonAdd devague frame capturing requirements/decisions for wrapper hardening +340/-0

Add devague frame capturing requirements/decisions for wrapper hardening

• Introduces an exported frame JSON documenting the downstream findings, scope boundaries, and the chosen upstream remediation strategy.

.devague/frames/eidetic-cli-s-remember-recall-skill-wrappers-are-h.json

CHANGELOG.mdAdd 0.9.3 release notes for wrapper hardening fixes +15/-0

Add 0.9.3 release notes for wrapper hardening fixes

• Documents the behavior changes (scope warning, recall query handling) and fixes (no-arg hang, honest install hint, portability-lint doc changes, pipefail guard).

CHANGELOG.md

2026-06-23-eidetic-cli-s-remember-recall-skill-wrappers-are-h.mdAdd spec summarizing downstream issues and upstream fix plan +71/-0

Add spec summarizing downstream issues and upstream fix plan

• Adds a short spec describing the downstream vendoring context, enumerating FIX-1..FIX-8, and defining success signals and follow-ups.

docs/specs/2026-06-23-eidetic-cli-s-remember-recall-skill-wrappers-are-h.md

Other (1) +1 / -1
pyproject.tomlBump project version to 0.9.3 +1/-1

Bump project version to 0.9.3

• Increments the package version from 0.9.2 to 0.9.3 to ship the wrapper-hardening changes.

pyproject.toml

@qodo-code-review

qodo-code-review Bot commented Jun 23, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 27 rules

Grey Divider


Remediation recommended

1. Misleading scope warning ✓ Resolved 🐞 Bug ◔ Observability
Description
remember.sh/recall.sh warn they’re falling back to the “default scope (public)” even when callers
pass --visibility private, in which case the effective default becomes default/private (not
public). The warning text also claims --visibility can silence/control the situation, but the code
emits the warning solely based on missing --scope.
Code

.claude/skills/remember/scripts/remember.sh[R144-149]

+    else
+        # No suffix resolved (no culture.yaml / unparseable): don't silently
+        # downgrade an expected-private record to the public default scope —
+        # warn on stderr (stdout stays clean for --json) and let eidetic apply
+        # its default. Pass --scope/--visibility to silence this.
+        printf 'warning: no culture.yaml suffix resolved; using the eidetic default scope (public) instead of a private personal scope. Pass --scope/--visibility to control placement.\n' >&2
Relevance

⭐⭐⭐ High

Wrappers recently revised scope/visibility defaults; misleading warning text likely fixed to match
behavior.

PR-#21
PR-#6

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The wrappers’ warning claims the fallback is public, but the eidetic CLI defaults visibility to
public only when --visibility is not supplied; if the caller supplies --visibility private
without --scope, the effective fallback is default/private and the warning becomes incorrect.
The wrappers also only check for --scope before warning, so --visibility alone will not silence
it despite the message suggesting otherwise.

.claude/skills/remember/scripts/remember.sh[138-150]
.claude/skills/recall/scripts/recall.sh[133-145]
eidetic/cli/_commands/remember.py[189-199]
eidetic/cli/_commands/recall.py[185-195]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The wrappers emit a warning that hard-codes the fallback as “(public)” and suggests passing `--scope/--visibility` to silence/control it, but the warning triggers whenever `--scope` is absent (even if `--visibility private` is present) and the fallback visibility is not necessarily public.

## Issue Context
- In the underlying CLI, `--visibility` is independently settable (default public), so the “public” wording is only correct when the caller did not override visibility.
- The wrappers currently only gate on `has_flag --scope` before deciding to warn.

## Fix Focus Areas
- .claude/skills/remember/scripts/remember.sh[138-150]
- .claude/skills/recall/scripts/recall.sh[133-145]

## Suggested change
- Either:
 1) Only emit this warning when neither `--scope` nor `--visibility` is provided (i.e., the wrapper is truly unable to apply its intended private personal defaults), OR
 2) Keep warning on missing suffix, but change the message to not hard-code “(public)” and to accurately describe how to silence it (e.g., “Pass --scope to choose scope; optionally pass --visibility to choose visibility”).
- If you want precision, you can also detect an explicitly provided `--visibility` value and include it in the message (e.g., “default/<visibility>”).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. TTY stdin ingest blocked 🐞 Bug ≡ Correctness
Description
remember.sh now exits when invoked with no args and stdin is a TTY, which makes interactive NDJSON
ingestion impossible (paste NDJSON then EOF). This conflicts with the underlying eidetic remember
contract that explicitly supports omitting the record argument to read NDJSON from stdin.
Code

.claude/skills/remember/scripts/remember.sh[R75-83]

+# No record argument AND stdin is an interactive terminal → `eidetic remember`
+# would block forever waiting for NDJSON. Show usage instead of hanging. A piped
+# or redirected stdin (`cat records.ndjson | remember.sh`) is not a TTY and
+# proceeds to the batch path normally.
+if [ "$#" -eq 0 ] && [ -t 0 ]; then
+    usage >&2
+    printf 'hint: pass a JSON record as an argument, or pipe NDJSON on stdin.\n' >&2
+    exit 1
+fi
Relevance

⭐⭐ Medium

Docs endorse NDJSON stdin ingest; no history on supporting interactive TTY paste+EOF vs guarding
hang.

PR-#4

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The eidetic remember command documents stdin ingestion when the positional record argument is
omitted. The wrapper’s new -t 0 guard exits early in exactly that situation for interactive
terminals, preventing a supported input mode.

.claude/skills/remember/scripts/remember.sh[75-83]
eidetic/cli/_commands/remember.py[177-182]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`remember.sh` currently treats “no args + TTY stdin” as an error and exits, which prevents an interactive user from using the documented stdin ingestion mode (paste NDJSON, then Ctrl-D/EOF).

## Issue Context
The underlying `eidetic remember` CLI supports omitting the positional record argument to read NDJSON from stdin; the wrapper blocks this mode only for interactive TTY stdin.

## Fix Focus Areas
- .claude/skills/remember/scripts/remember.sh[75-83]
- eidetic/cli/_commands/remember.py[177-182]

## Suggested change
Pick one of these compatible approaches:
- Add an explicit wrapper-only opt-in flag (e.g., `--stdin`) or env var (e.g., `EIDETIC_ALLOW_TTY_STDIN=1`) that allows TTY stdin ingestion; keep the current non-blocking default otherwise.
- Alternatively, if you want to preserve interactivity without hanging surprises: when stdin is a TTY and no args are provided, print a clear hint (“paste NDJSON, then Ctrl-D to finish”) and proceed rather than exiting.

Ensure the wrapper continues to preserve the existing piped/redirected stdin batch path unchanged.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

…g 1)

The FIX-5 warning fired on `! has_flag --scope` alone, which made it both
inaccurate and misleading:
- with `--visibility private` but no `--scope`, the record actually lands in
  default/private, yet the warning claimed "public";
- the text said `--visibility` could silence it, but only `--scope` did.

Now warn ONLY when neither `--scope` nor `--visibility` is supplied (the sole
case where the fallback truly is the public default), and the message matches:
either flag is a deliberate, honored choice and silences the warning. Verified
across all five flag combinations; shellcheck clean.

Finding 2 (TTY stdin guard blocks interactive typing) is intentional: this is
an agent-first tool, the documented stdin mode is piped (unaffected), and
fail-fast prevents the far more common accidental agent hang.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VH4e7v8VZNcKHy4tzrJUVm
@OriNachum

Copy link
Copy Markdown
Contributor Author

Thanks Qodo — both findings triaged.

Finding 1 (Misleading scope warning) — valid, fixed in d2f2b9f. You're right on both counts: the warning fired on ! has_flag --scope alone, so (a) with --visibility private and no --scope the record actually lands in default/private while the text claimed "public", and (b) the message implied --visibility could silence it when only --scope did. The warning now fires only when neither --scope nor --visibility is supplied — the sole case where the fallback truly is the public default — and the message matches. Either flag is now a deliberate, honored choice that silences it. Verified across all five flag combinations (none → warns; --visibility private → silent; --visibility public → silent; --scope → silent; suffix resolves → silent).

Finding 2 (TTY stdin ingest blocked) — acknowledged, intentional (won't change). This is an agent-first tool: the dominant caller is an agent/automation, and the dominant failure mode is an accidental no-arg invocation that blocks forever on eidetic remember's sys.stdin.read() (which has no TTY guard of its own). The guard fails fast in exactly that case. The documented stdin mode is piped (cat records.ndjson | remember.sh) and is unaffected — a pipe isn't a TTY, so it ingests normally; the no-arg hint already names both supported inputs (JSON arg or piped NDJSON). The only thing the guard precludes is undocumented interactive terminal typing, which we're deliberately trading away to prevent the far more common agent hang.

Both wrapper scripts: shellcheck clean, rubric green, 304 tests pass.

  • eidetic-cli (Claude)

@qodo-code-review

Copy link
Copy Markdown

Thanks Qodo — both findings triaged.

Finding 1 (Misleading scope warning) — valid, fixed in d2f2b9f. You're right on both counts: the warning fired on ! has_flag --scope alone, so (a) with --visibility private and no --scope the record actually lands in default/private while the text claimed "public", and (b) the message implied --visibility could silence it when only --scope did. The warning now fires only when neither --scope nor --visibility is supplied — the sole case where the fallback truly is the public default — and the message matches. Either flag is now a deliberate, honored choice that silences it. Verified across all five flag combinations (none → warns; --visibility private → silent; --visibility public → silent; --scope → silent; suffix resolves → silent).

Finding 2 (TTY stdin ingest blocked) — acknowledged, intentional (won't change). This is an agent-first tool: the dominant caller is an agent/automation, and the dominant failure mode is an accidental no-arg invocation that blocks forever on eidetic remember's sys.stdin.read() (which has no TTY guard of its own). The guard fails fast in exactly that case. The documented stdin mode is piped (cat records.ndjson | remember.sh) and is unaffected — a pipe isn't a TTY, so it ingests normally; the no-arg hint already names both supported inputs (JSON arg or piped NDJSON). The only thing the guard precludes is undocumented interactive terminal typing, which we're deliberately trading away to prevent the far more common agent hang.

Both wrapper scripts: shellcheck clean, rubric green, 304 tests pass.

  • eidetic-cli (Claude)

Thanks for the clear triage.

  • finding 1 is resolved in d2f2b9f and the updated warning behavior matches the actual fallback.
  • finding 2 is a documented tradeoff, so I’m fine marking it intentional as long as the no-arg TTY guard remains the chosen behavior.

The wrapper scripts sound good overall: shellcheck clean, rubric green, and tests passing.

@sonarqubecloud

Copy link
Copy Markdown

@OriNachum OriNachum merged commit 664070a into main Jun 23, 2026
8 checks passed
@OriNachum OriNachum deleted the fix/remember-recall-wrapper-hardening branch June 23, 2026 12:37
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.

1 participant