Summary
.agent/harness/hooks/claude_code_post_tool.py (the PostToolUse episodic
logging hook, v0.8+) persists raw, unredacted tool-call data into
.agent/memory/episodic/AGENT_LEARNINGS.jsonl — a durable, cross-session log
that's meant to be shared, diffed, and eventually exported via the data
flywheel.
Two distinct leak sites in the current hook:
- Absolute paths persisted verbatim.
_action_label() and the
parts.append(...) calls in the reflection builder use
tool_input.get("file_path") directly — for any file under the
operator's home directory, this bakes the literal /Users/<name>/... (or
equivalent) path into every action/reflection entry that touches a
file. No normalization to a portable form (~, project-relative) exists
anywhere in the hook.
- Raw content and edit diffs persisted. The
Edit/MultiEdit reflection
path does repr(old[:30]) / repr(new[:30]) — literal excerpts of the
edited content, not just that an edit happened. The generic fallback
branch in _reflection() (for tool types not explicitly handled — Read,
Grep, Glob, WebFetch, etc.) does json.dumps(tool_input) and embeds it
whenever the result is under 80 chars — which is exactly backwards: a
short absolute path or a short sensitive value is the case most likely
to leak in full, since nothing gets truncated below that threshold.
Reproduction
payload = {
"tool_name": "Edit",
"tool_input": {
"file_path": "/Users/alice/supabase/secrets.env",
"old_string": "STRIPE_SECRET_KEY=sk_live_abc123",
"new_string": "STRIPE_SECRET_KEY=sk_live_def456",
},
"tool_response": {"output": "", "exit_code": 0, "error": ""},
}
Piped through the current hook, the resulting AGENT_LEARNINGS.jsonl entry
contains /Users/alice/... in action, and both secret values verbatim in
reflection (repr(old[:30])/repr(new[:30])).
Fix
I have a patch ready (traced and verified locally, not yet opened as a PR —
wanted to check for an existing issue first): adds a small stdlib-only
_normalize_path() helper reusing the hook's own existing AGENT_ROOT
resolution (no new dependencies, no new config) to normalize paths to
project-relative or ~-relative form, replaces raw content/diff previews
with char counts, and fixes the _reflection() fallback branch to normalize
paths before its length check rather than embedding raw JSON. Extended
tests/test_claude_code_hook.py with new coverage (62/62 passing, up from
54/54) asserting no raw home path or file content survives into a persisted
entry, for both Edit and Write.
Related: filed the same-shaped bug against affaan-m/everything-claude-code's
skill-comply observation pipeline (skills/skill-comply/scripts/runner.py),
which has an analogous raw-payload-into-a-durable-artifact pattern. Will
cross-link once both issues exist.
Happy to open the PR — flagging as an issue first per your contribution norms.
Summary
.agent/harness/hooks/claude_code_post_tool.py(thePostToolUseepisodiclogging hook,
v0.8+) persists raw, unredacted tool-call data into.agent/memory/episodic/AGENT_LEARNINGS.jsonl— a durable, cross-session logthat's meant to be shared, diffed, and eventually exported via the data
flywheel.
Two distinct leak sites in the current hook:
_action_label()and theparts.append(...)calls in the reflection builder usetool_input.get("file_path")directly — for any file under theoperator's home directory, this bakes the literal
/Users/<name>/...(orequivalent) path into every
action/reflectionentry that touches afile. No normalization to a portable form (
~, project-relative) existsanywhere in the hook.
Edit/MultiEditreflectionpath does
repr(old[:30])/repr(new[:30])— literal excerpts of theedited content, not just that an edit happened. The generic fallback
branch in
_reflection()(for tool types not explicitly handled — Read,Grep, Glob, WebFetch, etc.) does
json.dumps(tool_input)and embeds itwhenever the result is under 80 chars — which is exactly backwards: a
short absolute path or a short sensitive value is the case most likely
to leak in full, since nothing gets truncated below that threshold.
Reproduction
Piped through the current hook, the resulting
AGENT_LEARNINGS.jsonlentrycontains
/Users/alice/...inaction, and both secret values verbatim inreflection(repr(old[:30])/repr(new[:30])).Fix
I have a patch ready (traced and verified locally, not yet opened as a PR —
wanted to check for an existing issue first): adds a small stdlib-only
_normalize_path()helper reusing the hook's own existingAGENT_ROOTresolution (no new dependencies, no new config) to normalize paths to
project-relative or
~-relative form, replaces raw content/diff previewswith char counts, and fixes the
_reflection()fallback branch to normalizepaths before its length check rather than embedding raw JSON. Extended
tests/test_claude_code_hook.pywith new coverage (62/62 passing, up from54/54) asserting no raw home path or file content survives into a persisted
entry, for both
EditandWrite.Related: filed the same-shaped bug against
affaan-m/everything-claude-code'sskill-complyobservation pipeline (skills/skill-comply/scripts/runner.py),which has an analogous raw-payload-into-a-durable-artifact pattern. Will
cross-link once both issues exist.
Happy to open the PR — flagging as an issue first per your contribution norms.