Repository navigation
docs: feasibility spike — Claude Code 2.1.263 as the E01 pilot host - #416
Conversation
One host, one client, disposable fixtures, no global configuration changed, no external provider spend, nothing registered. Evidence for the maintainer's D1–D5, per requirement: 1. Presentation: the client declares elicitation.form even headless; elicitInput headless returns cancel (nothing minted); an interactive dialog exists (bundle); an Elicitation hook can answer for the user (bundle; not exercised). The live human check is still required and its exact steps are written down. 2. Namespace: permissions.deny on Write/Edit alone is bypassed by python3 (observed); the OS sandbox with filesystem.denyWrite refuses every shell path with EROFS (observed). No host mechanism attests protection continuity — every acceptance on this host is cooperative under the contract until D3 settles a mechanism (candidate given). 3. Tool results: PostToolUse/PostToolUseFailure hooks run outside the sandbox and delivered tool_use_id, command, stdout/stderr, and "Exit code N" into a denyWrite inbox the model could neither write nor make the hook script write (observed). Hook config protection comes from the sandbox's protected paths and the permission layer's protected paths, except in bypassPermissions. 4. Unsupported behaviour is honest at each layer. Smallest feasible pilot configuration proposed (user/managed settings, no bypassPermissions, no Elicitation hooks, PostToolUse+Failure hooks into the protected inbox). Fixtures under spikes/, outside core, tests, tsconfig, and the npm files list. Refs #399 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…onfiguration Maintainer review of the spike found that the proposed pilot configuration denied writes to the namespace but not to the hook script, which runs unsandboxed on every Bash call: a sandboxed model could rewrite it and forge observations while escaping the sandbox. E4 had tested running the script and rewriting settings, not editing its body. Corrected: the hook is hardened (absolute interpreter, PATH=/usr/bin:/bin, absolute command paths, nothing sourced) and settings-E5 puts the hook directory and the settings file itself in denyWrite with matching Write/Edit deny rules. E5 (observed, 13 Bash calls): append, replace, create, chmod, rename, and symlink attacks on the hook all refused (EROFS / EBUSY); the settings file, /usr/bin, the interpreter, and every PATH directory refused; hashes unchanged; the hook still fired 13 times. Finding: the parent directory, under the per-user temp root, IS writable from the sandbox — only the explicit denyWrite protected the siblings, so the pilot configuration now requires <ingest> and the settings file in denyWrite explicitly and never under /tmp or the working directory. tool_result_path: protected is claimed only for the E5 configuration; the D3 candidate is reworked so the initialization marker is written only by a script that is itself protected, with the SessionStart visibility left as an E02 investigation. README path mismatch fixed; fixtures verified to reproduce via envsubst. No production code; registry empty; cooperative unauthorized. Refs #399 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Revision: E5 (protect the ingestion hook itself)Maintainer review found the first proposed configuration denied writes to the namespace but not to the hook script, which runs unsandboxed on every Bash call — a rewritten script would forge observations and escape the sandbox in one step. E4 had tested running the script and editing settings, not editing the script body. E4's configuration is withdrawn. Corrected fixture E5 (observed, 13 Bash calls, hashes compared before/after):
Script and settings hashes unchanged after the run. Consequences in the report: Head: |
…cted Second maintainer finding of the same class: the E5 hook still wrote its error and status logs into the parent directory the model can write, and the hook runs outside the sandbox, so a planted symlink turned its own redirect into an unsandboxed write primitive. E6a (observed, separate disposable root, against the E5 hook): with hook-err.log symlinked to the script, the hook truncated its own script to 0 bytes on the next Bash call (ingestion DoS); with hook-status.log symlinked into the inbox, three "hook-wrote-0" lines landed at an attacker-chosen path inside the evidence namespace. The E5 hook is withdrawn. Corrected hook: every write — payload, stderr, status — inside the namespace, one fresh uniquely named file per event, set -C so each ">" is O_CREAT|O_EXCL and refuses any symlink (proved locally under this host's /usr/bin/sh for dangling and existing links), no ">>" anywhere. E6b (observed): symlinks at every path the hook writes refused with EROFS; symlinks at the old paths planted but never opened; a symlink from the writable cwd into the namespace is refused on the resolved path (confirmed with bwrap --ro-bind directly); script intact; 10 inbox and 20 log files, all hook-written and uniquely named; no symlink inside the namespace. Report: the requirement is generalized — tool_result_path: protected needs every path the hook reads, executes, AND writes to be protected — and carried into the D4 answer, the pilot configuration, and the E05 note. Still: registry empty, cooperative unauthorized, no production code, #399 open. Refs #399 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Revision: E6 (every path the unsandboxed hook writes)Second finding of the same class: the E5 hook still wrote E6a — the primitive, against the E5 hook (separate disposable root):
Corrected hook: every write (payload, stderr, status) inside the namespace; one fresh uniquely named file per event; E6b — against the corrected hook (observed): symlinks at Requirement, generalized (report § requirement 3, § D4, pilot configuration, E05 note): Head: |
Bounded feasibility spike for E01 (#399) against one host/client: Claude Code 2.1.263 on Linux as the MCP client. Registers nothing —
VERIFIED_ACCEPTANCE_INTEGRATIONSstays empty,cooperativestays unauthorized, no E02/E05/E06/E07 code. Docs + disposable fixtures only; nothing undercore/,tests/,mcp-server/, orextensions/changes, andspikes/is outsidetsconfigand the npmfileslist. Do not merge until reviewed.Report:
docs/engineering/spike-claude-code-2026-09-18.md. Fixtures:spikes/claude-code-2.1.263/(parametrized; no home or scratch paths committed).What was tested and what actually happened
All runs headless (
claude -p) in a disposable directory with--settings <file>and--mcp-config … --strict-mcp-config; no user/managed settings, hooks, MCP registrations, or sandbox state on the machine were changed. Each claim is marked observed / bundle / docs in the report.elicitation.formatinitializeeven headless; headlesselicitInput→{ action: "cancel" }, nothing minted. An interactive dialog exists (bundle strings). AnElicitationhook can answer for the user (bundle schema) — hook config is part of the boundary; not exercised (classifier refused; would be a simulated acceptance). The live human check was not performed; exact steps are written down.permissions.denyonWrite/Editalone:python3 -c "open(…).write()"wrote the file (observed). OS sandbox +filesystem.denyWrite: redirect, python, cp, mkdir allEROFS(observed); config files are sandbox- and permission-protected except inbypassPermissions(docs). No host mechanism attests continuity → every acceptance on this host iscooperativeunder the contract until D3 settles a mechanism; a candidate is proposed.PostToolUse/PostToolUseFailurehooks run outside the sandbox and deliveredtool_use_id, command, stdout/stderr, andExit code Ninto adenyWriteinbox; the model's Bash could neither write the inbox nor make the hook script write (observed). Limitation: failures arrive onPostToolUseFailurewith the exit code as text.cancel; unregistered pair →needs-human-acceptance; missing sandbox deps → hard failure withfailIfUnavailable(docs); unprotected hook path →caller; unknown history →cooperative.Smallest feasible pilot configuration (proposal)
User-level or managed settings, never only a project file, never
bypassPermissions: sandbox enabled withallowUnsandboxedCommands: false,failIfUnavailable: true,filesystem.denyWriteon the namespace;Write/Editdeny rules on it;PostToolUse+PostToolUseFailurehooks (matcherBash) into a host-side ingestion entry writing to the protected inbox; noElicitation/ElicitationResulthooks; interactive sessions only for acceptance. Provides per sessionstorage_boundary: host-enforcedandtool_result_path: protected; provideshuman_acceptance: mcp-elicitationunverified until the live check; does not provideprotection: continuous-since-initialization.Still unresolved
Verification:
npm run buildexit 0;npm test971 pass / 0 fail (unchanged code);git diff --checkclean; staged content scanned for usernames, home paths, and scratch paths — none.Refs #399, #398
🤖 Generated with Claude Code