End-to-end test fixture for ctxcop's Claude Code ElicitationResult
hook. Originally built for #3;
see RESULT.md for that run's findings.
The fixture exists for regression / forward-compat testing: re-run
against future Claude Code releases to confirm that elicitation
content still doesn't reach the model context, and to capture any
new payload shape changes (the kind that produced
#5).
A minimal MCP server exposes two tools, both of which call ctx.elicit()
with the same credential-shape schema. They differ only in what the
tool response returns:
| Tool | Tool response | Isolates |
|---|---|---|
echo_form_neutral |
"form_submitted_neutral" |
Whether content itself reaches the transcript, independent of the tool response. |
echo_form_verbatim |
the submitted value, echoed back | Sanity check that the sentinel mechanism works. |
The test sentinel is the literal value held in server.py's SENTINEL
constant (split-literal in source so the file itself can be edited
under ctxcop). It's credential-shape (matches ctxcop's
ctxcop-aws-access-key rule, base32-alphabet compliant) and uniquely
identifiable for grep.
To see the sentinel without ctxcop redacting it:
.venv/bin/python -c 'from server import SENTINEL; print(SENTINEL)'The local .ctxcop.toml allowlists the literal so docs in this
directory can mention it; the global ruleset still detects it
anywhere else.
From this directory:
uv syncThat installs the mcp SDK into .venv/. .mcp.json invokes
uv run python server.py from cwd.
Open a new Claude Code session in this directory (separate from whatever you're using now):
claude(Approve the MCP server when prompted; .claude/settings.json
sets enableAllProjectMcpServers: true and CTXCOP_AUDIT_LOG=./ctxcop-audit.jsonl.)
In the Claude session:
Call the
echo_form_neutraltool from the ctxcop-elicit-test MCP server. Wait for me to fill in the form manually.
When Claude Code shows the elicitation form, type the sentinel into
the api_key field and submit.
Now call
echo_form_verbatim.
Type the sentinel into the api_key field again.
Without repeating the literal value: for each of the two form submissions (neutral and verbatim), do you have visibility into the api_key value I entered? Answer per-variant with 'visible' or 'not visible' and briefly cite which message in your context contains it.
This is a third signal alongside the transcript grep.
Run these from this directory after the session. The sentinel is captured into a shell variable so the verify commands don't contain the literal:
S=$(.venv/bin/python -c 'from server import SENTINEL; print(SENTINEL)')
# Claude Code encodes cwd into the transcript dir name by replacing
# slashes with dashes. The most-recently-modified match is the active
# session's transcript.
TRANSCRIPT_DIR=~/.claude/projects/$(pwd | sed 's|/|-|g')
ls -lt "$TRANSCRIPT_DIR"
# Grep the transcript(s) for the sentinel.
grep -l "$S" "$TRANSCRIPT_DIR"/*.jsonl
# For each hit, see which message type contains it.
grep -h "$S" "$TRANSCRIPT_DIR"/*.jsonl \
| jq -c '{type, role: (.message.role // null), preview: ((.message.content // .) | tostring | .[0:240])}'
# What did ctxcop see (via the audit log we configured)?
cat ./ctxcop-audit.jsonl | jq -c '.' || echo "no audit log written"| Neutral-variant sentinel in transcript? | Verbatim-variant sentinel in transcript? | Conclusion |
|---|---|---|
| Yes | Yes (expected) | content reaches the model context. Escalate ElicitationResult to active mutation. |
| No | Yes | content does NOT reach the model context. Observe-only is correct; no change to ctxcop. |
| No | No | Test methodology broken (sentinel filter, transport encoding, or transcript layout differs). Diagnose before drawing conclusions. |
In all cases:
- The ctxcop audit log should show one
form-credential-detectedentry per tool invocation, namingapi_keyas the flagged field andctxcop-aws-access-keyas the rule. This confirms the ElicitationResult hook fired — independent of whatever Claude did with the value.