Skip to content

release: v0.47.0 — shell noFollowSymlinks, resolveToolPath, shared primitives generator core - #54

Merged
hamr0 merged 13 commits into
mainfrom
feat/shell-nofollow-symlinks
Sep 28, 2026
Merged

hamr0 merged 13 commits into
mainfrom
feat/shell-nofollow-symlinks

Conversation

@hamr0

@hamr0 hamr0 commented Sep 28, 2026

Copy link
Copy Markdown
Owner

v0.47.0

  • createShellTools({ noFollowSymlinks: true }) (opt-in): read/write/edit/grep refuse a symlinked final path component at open time (ELOOP). Requested by fwdloop.
  • resolveToolPath(p) — new export from bare-agent/tools: the canonicalizer every shell file tool applies (~ via os.homedir(), then path.resolve), so a gate judges the same string the tool opens.
  • Bounded reads — no OOM on /dev/zero or huge size-0 (procfs) files.
  • Shared primitives.json generator core — scripts/primitives-core.mjs, vendored byte-identically into bareguard and litectx, per-repo primitives.config.mjs, SHA pin test. One rule for @-lines: a tag at the normal position, or a hard error (inside @example, always an error). Duplicate names error. primitives.json byte-identical in all three repos.

Reviewed (/branch-review, ready, no blockers) and /ship green: 1297 pass / 0 fail, typecheck, build, check:primitives.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RNVyxYsxAMaxUW1wZk1H4b

hamr0 and others added 13 commits September 28, 2026 11:20
- createShellTools({ noFollowSymlinks: true }): shell_read/write/edit/grep
  refuse a symlinked FINAL path component at open time (O_NOFOLLOW, atomic;
  Windows lstat fallback, non-atomic). Throws err.code 'ELOOP'. Dangling-link
  writes refused, target never created. One shared open helper; grep root
  opened atomically; dev/ino recheck narrows the dir-listing race.
- resolveToolPath(p): the one canonicalizer (~ via os.homedir(), then
  path.resolve) the tools open with, exported from bare-agent/tools so the
  gate checks the same string the tool opens (bareguard 0.19.0 split).
- Fixed: empty HOME turned ~/x into /x; now throws. Non-string/empty paths
  throw a type-only error instead of defaulting to cwd.
- Parent-chain containment stays bareguard's fs.resolveSymlinks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNVyxYsxAMaxUW1wZk1H4b
- shell_read/shell_edit read through the handle to EOF instead of trusting
  stat.size, so procfs/sysfs files (size 0, real content) read correctly
  with noFollowSymlinks on or off. Regression from 136a00c; /proc tests added.
- Docs no longer cite unpublished bareguard option names; state the
  resolved-path containment check is upcoming and the gate is lexical today.
- Document that shell_run/shell_exec cwd is not covered by
  noFollowSymlinks or resolveToolPath.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNVyxYsxAMaxUW1wZk1H4b
- New readBounded(fh, limit): chunked read that stops at EOF or limit+1
  bytes, whatever stat.size claims. shell_read on an unknown-size file
  truncates honestly ("file size unknown"); shell_edit refuses a file over
  its maxBytes cap before editing (no change made).
- /dev/zero no longer OOM-crashes the process (it did in 0.46.6 via
  fs.readFile, and in 1d5b6f6 via fh.readFile); /proc/kallsyms no longer
  loaded whole. Child-process tests, mutation-proved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNVyxYsxAMaxUW1wZk1H4b
…runcated)

The multi-line @when/@fails tags were cut to their first line in
primitives.json, and the @when wrongly said bareguard canonicalizes the
path (the executor does). Rewritten as single accurate lines, unpublished
bareguard version number dropped, manifest regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNVyxYsxAMaxUW1wZk1H4b
parseBlock read only the tag's first line, so a wrapped @when/@fails was
silently truncated in primitives.json while check:primitives still passed
(it compares against its own truncated output). Now a continuation line is
a loud problem (non-zero exit). Fixture tests, mutation-proved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNVyxYsxAMaxUW1wZk1H4b
…try doc

- A wrapped @when/@fails line that itself starts with `@word` was parsed
  as an unknown tag and silently dropped, so the truncation slipped past
  89b693d's continuation check. Unknown tags inside a @when block are now
  a loud problem, against a KNOWN_TAGS set scanned from the repo's own
  @when blocks. Fixture test, mutation-proved.
- CHANGELOG: drop the false claim that resolveToolPath's truncation
  shipped in 0.46.6 (it was caught on this branch before release).
- Retry @when rewritten as a complete one-line sentence; CLAUDE.md
  primitives count 51 -> 52.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNVyxYsxAMaxUW1wZk1H4b
A tag body is every line up to the next tag. Rule A: multi-line @when/@fails/@category/@name/@Signature fails. Rule B: unknown @tag in a @when block fails, with alias hints. @example keeps tag-shaped code lines. primitives.json unchanged. Mirrors bareguard 73530ae.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…SHA pin

One generator core (scripts/primitives-core.mjs), vendored byte-identically
into bareguard and litectx, with a per-repo primitives.config.mjs and a
CORE_SHA256 pin test (test/primitives-core.test.mjs) that fails on drift.
Rules: JSDoc tag bodies, single-line when/fails/category/name/signature,
unknown-tag rejection, and @example must be the last tag (a @param/@type
line inside an example no longer truncates it). Class methods, recursive
scan, Windows-safe paths, CRLF-safe hashing. primitives.json byte-identical
in all three repos.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNVyxYsxAMaxUW1wZk1H4b
…est attribution

A known tag after @example is now a hard error (move @example last) instead
of silently truncating the example or swallowing a real tag; any other @word
line stays example content. Duplicate catalog names fail loudly (suggests
@name Class#method). Docs corrected: the unknown-@word bug was litectx-only;
the known-tag truncation hole was in all three. createShellTools' two
@example tags merged into one (primitives.json byte-identical).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNVyxYsxAMaxUW1wZk1H4b
…rt with @

@example is the last tag; any line inside it that starts with "@" (any
spacing, case, alias, or word) is a hard error. Replaces the per-case
known-tag guessing that kept leaking (indented @PARAM, @return, @PARAM
slipped through silently). Tags elsewhere are matched on the trimmed line.
Net -27 lines. primitives.json byte-identical in all three repos
(bareguard after moving @example last in 4 gate.js blocks).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNVyxYsxAMaxUW1wZk1H4b
A line starting with "@" is either a tag at the normal position (right
after " * ") or a hard error; inside @example it is always an error.
Undoes 2152464's trimmed tag match, which let an indented "@category"
under @when silently overwrite the field. primitives.json byte-identical
in all three repos.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNVyxYsxAMaxUW1wZk1H4b
Decisions Log index (docs/index.md) had gone stale: two follow-on
commits in this range (2152464, da2ecd8) each appended new entries to
docs/wiki/decisions-log.md after the last index rebuild (fa13186),
without re-running it. The line count (212) and every heading's L-range
below the insertion point were off by up to 8 lines, and the new
"unreleased / shared primitives.json generator core" top-level entry
was missing from the index entirely. Recomputed all ranges from the
file's current headings.
…zer, bounded reads, shared primitives generator core

- createShellTools({ noFollowSymlinks: true }) opt-in symlink refusal for the four
  file tools; resolveToolPath(p) exported as the shared canonicalizer.
- shell_read/shell_edit read bounded (readBounded, 64KB chunks, cap+1 ceiling) —
  fixes empty reads from procfs/sysfs and an OOM crash on EOF-less device files.
- Shared vendored primitives.json generator core (scripts/primitives-core.mjs)
  across bare-agent/bareguard/litectx, with strict @example-last, duplicate-name,
  wrapped-tag, and unknown-@when-tag hard errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNVyxYsxAMaxUW1wZk1H4b
@hamr0
hamr0 merged commit 42e4aca into main Sep 28, 2026
2 checks passed
@hamr0
hamr0 deleted the feat/shell-nofollow-symlinks branch September 28, 2026 17:47
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