Skip to content

Privacy: claude_code_post_tool.py persists raw absolute paths and file content/diffs into AGENT_LEARNINGS.jsonl #66

Description

@diazMelgarejo

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:

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions