How to Audit Hidden Reminders and Context Usage in Claude Code Logs | Agent Lab Journal

  Agent Lab Journal

    Guides
    Glossary

Enter fullscreen mode Exit fullscreen mode

Advanced field guide

How to Audit Hidden Reminders and Context Usage in Claude Code Logs

      Advanced · 45 min read · Local analysis · Updated August 1, 2026



      The visible transcript in Claude Code is not necessarily a complete representation of everything recorded around a request. Service messages, internal reminder markers, tool payloads, and usage metadata can exist in session logs without appearing as ordinary chat turns. If you want to know how often ip_reminder occurs—or how input, output, cache creation, and cache read tokens are distributed—you need to inspect the stored records directly and preserve enough structure to avoid misleading totals.

Enter fullscreen mode Exit fullscreen mode

In this guide

  • What this audit can establish

  • Concrete investigation case

  • Locate and select one session

  • Preserve an auditable copy

  • Run a quick structural check

  • Build the full local report

  • Interpret reminder and token data

  • Verify the report independently

  • Failure cases and repairs

  • Limitations

What this audit can—and cannot—establish

      This workflow examines one local session stored as JSON Lines (JSONL): a text format in which each line is normally an independent JSON value. It creates a report with:

Enter fullscreen mode Exit fullscreen mode

  • the selected file’s path, size, modification time, and SHA-256 digest;

  • the number of physical lines, parsed records, blank lines, and malformed lines;

  • every record containing the exact, case-sensitive string ip_reminder;

  • the JSON paths at which the marker was found;

  • timestamps and record types when those fields are available;

  • per-record and aggregate input, output, cache creation, and cache read token values;

  • a chronological CSV suitable for a spreadsheet or notebook;

  • a machine-readable JSON report for later comparison.

      The report shows what is present in the selected file. It does not prove why a reminder was inserted, whether it was transmitted to a model exactly as stored, or how the client’s undocumented internal state behaved. Treat ip_reminder as an observable marker, not as a complete explanation of the request pipeline.
    
      The same distinction applies to the context window. Logged token counters can help characterize request activity, but they do not reconstruct the exact context visible to the model at every instant. Cache reads, retries, branching, compaction, and duplicated usage objects can all invalidate a naive sum.
    

Concrete case: a long coding session feels unexpectedly expensive

      Imagine a long refactoring session. The visible conversation contains a few dozen user and assistant turns, but the session log is much larger than expected. You suspect two things:

Enter fullscreen mode Exit fullscreen mode

  • the client is recording repeated ip_reminder insertions that are not obvious in the interface;

  • large cache reads or repeated input processing—not assistant output—account for most of the recorded usage.

      The useful question is not merely “Does the marker exist?” It is:
    

Where does the marker occur, how are its occurrences distributed through the session, and what token-usage records appear around the same points?

      A defensible answer needs record-level evidence. Counting raw text matches alone is insufficient because one record can contain the string multiple times, a field can quote an earlier payload, and the same API usage object can be repeated in several wrapper records.

Enter fullscreen mode Exit fullscreen mode

Prerequisites and safety

      You need a shell, Python 3.9 or later, and read access to your local Claude Code data. The optional spot checks use rg, jq, find, sort, sha256sum, and wc. On macOS, shasum -a 256 can replace sha256sum.



      Session files can contain source code, prompts, tool results, local paths, environment details, and other sensitive material. Keep the report local. Do not paste raw records into an issue, public repository, or shared chat without reviewing and redacting them.

Enter fullscreen mode Exit fullscreen mode

Work on a copy

        Do not modify the live session file. Claude Code may still be writing to it, and even a harmless formatter would destroy the one-record-per-line layout needed for reliable auditing.

Enter fullscreen mode Exit fullscreen mode

Step 1: locate and select exactly one session

      Installations and versions can organize local data differently, so begin with discovery rather than assuming one fixed path. The following command searches the conventional user-level Claude directory for JSONL files and prints their modification time and path:

Enter fullscreen mode Exit fullscreen mode

find "$HOME/.claude" -type f -name '*.jsonl' -printf '%T@ %p\n' 2>/dev/null \
  | sort -nr \
  | head -n 30

Enter fullscreen mode Exit fullscreen mode

      BSD find, commonly used on macOS, does not support -printf. Use:

Enter fullscreen mode Exit fullscreen mode

find "$HOME/.claude" -type f -name '*.jsonl' -print0 2>/dev/null \
  | xargs -0 stat -f '%m %N' \
  | sort -nr \
  | head -n 30

Enter fullscreen mode Exit fullscreen mode

      If the directory is absent, search a limited part of your home directory rather than the entire filesystem:

Enter fullscreen mode Exit fullscreen mode

find "$HOME" -maxdepth 5 -type f \
  \( -name '*.jsonl' -o -name '*.json' \) \
  2>/dev/null \
  | rg '/\.claude/|claude|projects|sessions'

Enter fullscreen mode Exit fullscreen mode

      Choose the file using evidence:

Enter fullscreen mode Exit fullscreen mode

  • its modification time overlaps the session you want;

  • its project path or working-directory fields match the project;

  • its early user text matches a prompt you remember;

  • its session identifier remains consistent across the file.

      Set an explicit variable. Quote it in every command:
    
SESSION_FILE="/absolute/path/to/the/selected-session.jsonl"

test -f "$SESSION_FILE" || {
  printf 'Session file not found: %s\n' "$SESSION_FILE" >&2
  exit 1
}

wc -l -c "$SESSION_FILE"
file "$SESSION_FILE"

Enter fullscreen mode Exit fullscreen mode

      Do not select a file solely because it is the newest. A background process, a second terminal, or a brief later session may have a more recent timestamp.

Enter fullscreen mode Exit fullscreen mode

Preview structure without dumping content

      Inspect only field names and common metadata first:

Enter fullscreen mode Exit fullscreen mode

head -n 5 "$SESSION_FILE" \
  | jq -c '{
      keys: (keys | sort),
      type: (.type // .kind // .event // null),
      timestamp: (.timestamp // .created_at // .createdAt // null),
      session_id: (.sessionId // .session_id // null)
    }'

Enter fullscreen mode Exit fullscreen mode

      If this fails, the file may contain malformed lines, a JSON array instead of JSONL, or non-JSON prefixes. Do not “repair” the source yet; the reporting script below records malformed lines without altering the evidence.

Enter fullscreen mode Exit fullscreen mode

Step 2: preserve an auditable snapshot

      A moving source file creates irreproducible counts. Finish or pause activity in the selected session, then create a private working directory and copy the file:

Enter fullscreen mode Exit fullscreen mode

umask 077
AUDIT_DIR="$PWD/claude-session-audit"
mkdir -p "$AUDIT_DIR"

cp -p "$SESSION_FILE" "$AUDIT_DIR/session.jsonl"

sha256sum "$AUDIT_DIR/session.jsonl" \
  | tee "$AUDIT_DIR/session.sha256"

wc -l -c "$AUDIT_DIR/session.jsonl" \
  | tee "$AUDIT_DIR/session.size.txt"

Enter fullscreen mode Exit fullscreen mode

      On macOS:

Enter fullscreen mode Exit fullscreen mode

shasum -a 256 "$AUDIT_DIR/session.jsonl" \
  | tee "$AUDIT_DIR/session.sha256"

Enter fullscreen mode Exit fullscreen mode

      From this point forward, analyze $AUDIT_DIR/session.jsonl. The hash identifies the exact bytes used to produce the report. If the live file changes later, your original result remains reproducible.

Enter fullscreen mode Exit fullscreen mode

Step 3: run a quick marker and schema check

      Start with a raw, case-sensitive search:

Enter fullscreen mode Exit fullscreen mode

rg -n -F -- 'ip_reminder' "$AUDIT_DIR/session.jsonl"

Enter fullscreen mode Exit fullscreen mode

      Count matching physical lines:

Enter fullscreen mode Exit fullscreen mode

rg -c -F -- 'ip_reminder' "$AUDIT_DIR/session.jsonl" || true

Enter fullscreen mode Exit fullscreen mode

      Count all literal occurrences, including repeated occurrences on one line:

Enter fullscreen mode Exit fullscreen mode

rg -o -F -- 'ip_reminder' "$AUDIT_DIR/session.jsonl" \
  | wc -l

Enter fullscreen mode Exit fullscreen mode

      These numbers answer different questions. A line count approximates matching records only when every physical line is one record. The occurrence count includes repeated mentions inside a record and can include quoted or escaped historical content.

Enter fullscreen mode Exit fullscreen mode

Find candidate usage fields

      Tokens are units used by the model and API accounting layers; they are not equivalent to characters or words. Search field names rather than assuming a single schema:

Enter fullscreen mode Exit fullscreen mode

rg -o --no-filename \
  '"[^"]*(input|output|cache)[^"]*tokens?[^"]*"' \
  "$AUDIT_DIR/session.jsonl" \
  | sort \
  | uniq -c \
  | sort -nr

Enter fullscreen mode Exit fullscreen mode

      Typical usage objects may contain names such as input_tokens, output_tokens, cache_creation_input_tokens, and cache_read_input_tokens. Other versions may use camelCase or nested structures. The script normalizes several common variants while retaining every source path.

Enter fullscreen mode Exit fullscreen mode

Why not sum everything with one jq expression?

      A command that recursively collects every numeric field named input_tokens can double-count the same usage object when the log stores it both in a request wrapper and a response event. Before producing totals, you must distinguish:

Enter fullscreen mode Exit fullscreen mode

  • raw usage objects found anywhere in a record;

  • identical usage objects repeated at different paths;

  • records that share a stable message or request identifier;

  • separate API calls that happen to have identical token values.

Step 4: build a schema-tolerant local report

      Save the following as audit_claude_session.py. It uses only the Python standard library. It never sends data over the network and does not include full record bodies in its outputs.

Enter fullscreen mode Exit fullscreen mode

#!/usr/bin/env python3
import argparse
import csv
import hashlib
import json
import os
import re
import sys
from collections import Counter, defaultdict
from datetime import datetime, timezone
from pathlib import Path

MARKER = "ip_reminder"

TOKEN_ALIASES = {
    "input_tokens": "input_tokens",
    "inputTokens": "input_tokens",
    "output_tokens": "output_tokens",
    "outputTokens": "output_tokens",
    "cache_creation_input_tokens": "cache_creation_tokens",
    "cacheCreationInputTokens": "cache_creation_tokens",
    "cache_creation_tokens": "cache_creation_tokens",
    "cacheCreationTokens": "cache_creation_tokens",
    "cache_read_input_tokens": "cache_read_tokens",
    "cacheReadInputTokens": "cache_read_tokens",
    "cache_read_tokens": "cache_read_tokens",
    "cacheReadTokens": "cache_read_tokens",
}

ID_KEYS = (
    "request_id", "requestId",
    "message_id", "messageId",
    "event_id", "eventId",
    "uuid", "id",
)

TIME_KEYS = (
    "timestamp", "created_at", "createdAt",
    "time", "date",
)

TYPE_KEYS = (
    "type", "kind", "event", "event_type", "eventType",
)

def sha256_file(path):
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for block in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(block)
    return digest.hexdigest()

def json_path(parts):
    out = "$"
    for part in parts:
        if isinstance(part, int):
            out += f"[{part}]"
        elif re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", str(part)):
            out += "." + str(part)
        else:
            out += "[" + json.dumps(str(part), ensure_ascii=False) + "]"
    return out

def walk(value, path=()):
    yield path, value
    if isinstance(value, dict):
        for key, child in value.items():
            yield from walk(child, path + (key,))
    elif isinstance(value, list):
        for index, child in enumerate(value):
            yield from walk(child, path + (index,))

def scalar_string(value):
    if isinstance(value, (str, int, float)) and not isinstance(value, bool):
        return str(value)
    return None

def find_first_scalar(record, keys):
    if isinstance(record, dict):
        for key in keys:
            if key in record:
                value = scalar_string(record[key])
                if value is not None:
                    return value
    for _, value in walk(record):
        if isinstance(value, dict):
            for key in keys:
                if key in value:
                    found = scalar_string(value[key])
                    if found is not None:
                        return found
    return None

def marker_hits(record):
    hits = []
    for path, value in walk(record):
        if isinstance(value, str):
            count = value.count(MARKER)
            if count:
                hits.append({
                    "path": json_path(path),
                    "occurrences": count,
                    "value_length": len(value),
                })
    return hits

def numeric_token(value):
    if isinstance(value, bool):
        return None
    if isinstance(value, int) and value >= 0:
        return value
    if isinstance(value, float) and value >= 0 and value.is_integer():
        return int(value)
    return None

def usage_objects(record):
    found = []
    for path, value in walk(record):
        if not isinstance(value, dict):
            continue
        normalized = {}
        source_keys = {}
        for key, raw in value.items():
            metric = TOKEN_ALIASES.get(key)
            number = numeric_token(raw)
            if metric is not None and number is not None:
                normalized[metric] = number
                source_keys[metric] = key
        if normalized:
            found.append({
                "path": json_path(path),
                "metrics": normalized,
                "source_keys": source_keys,
            })
    return found

def usage_fingerprint(metrics):
    ordered = {
        key: metrics.get(key)
        for key in (
            "input_tokens",
            "output_tokens",
            "cache_creation_tokens",
            "cache_read_tokens",
        )
        if key in metrics
    }
    return json.dumps(ordered, sort_keys=True, separators=(",", ":"))

def choose_record_usage(objects):
    """
    Collapse exact duplicate metric sets only within one record.

    This reduces obvious wrapper duplication but deliberately does not merge
    identical values from different records: those may be separate API calls.
    """
    unique = {}
    for item in objects:
        fingerprint = usage_fingerprint(item["metrics"])
        unique.setdefault(fingerprint, item)
    totals = Counter()
    for item in unique.values():
        totals.update(item["metrics"])
    return dict(totals), list(unique.values())

def main():
    parser = argparse.ArgumentParser(
        description="Audit ip_reminder and token usage in one JSONL session."
    )
    parser.add_argument("session", type=Path)
    parser.add_argument(
        "--out",
        type=Path,
        default=Path("claude-audit-report"),
    )
    args = parser.parse_args()

    source = args.session.expanduser().resolve()
    output = args.out.expanduser().resolve()

    if not source.is_file():
        raise SystemExit(f"Not a file: {source}")

    output.mkdir(parents=True, exist_ok=True)

    stat = source.stat()
    summary = {
        "report_version": 1,
        "generated_at_utc": datetime.now(timezone.utc).isoformat(),
        "source": {
            "path": str(source),
            "bytes": stat.st_size,
            "modified_at_utc": datetime.fromtimestamp(
                stat.st_mtime, timezone.utc
            ).isoformat(),
            "sha256": sha256_file(source),
        },
        "marker": MARKER,
        "physical_lines": 0,
        "blank_lines": 0,
        "parsed_records": 0,
        "malformed_lines": 0,
        "records_with_marker": 0,
        "literal_marker_occurrences": 0,
        "records_with_usage": 0,
        "raw_usage_objects": 0,
        "unique_usage_objects_within_records": 0,
        "token_totals_after_in_record_exact_deduplication": {
            "input_tokens": 0,
            "output_tokens": 0,
            "cache_creation_tokens": 0,
            "cache_read_tokens": 0,
        },
        "warnings": [],
    }

    marker_rows = []
    usage_rows = []
    malformed_rows = []
    record_type_counts = Counter()
    marker_path_counts = Counter()
    usage_path_counts = Counter()

    with source.open("r", encoding="utf-8", errors="replace") as handle:
        for line_number, line in enumerate(handle, start=1):
            summary["physical_lines"] += 1

            if not line.strip():
                summary["blank_lines"] += 1
                continue

            try:
                record = json.loads(line)
            except json.JSONDecodeError as exc:
                summary["malformed_lines"] += 1
                malformed_rows.append({
                    "line": line_number,
                    "error": str(exc),
                    "text_length": len(line),
                })
                continue

            summary["parsed_records"] += 1

            timestamp = find_first_scalar(record, TIME_KEYS)
            record_type = find_first_scalar(record, TYPE_KEYS)
            record_id = find_first_scalar(record, ID_KEYS)

            if record_type:
                record_type_counts[record_type] += 1

            hits = marker_hits(record)
            if hits:
                summary["records_with_marker"] += 1
                occurrence_count = sum(hit["occurrences"] for hit in hits)
                summary["literal_marker_occurrences"] += occurrence_count
                for hit in hits:
                    marker_path_counts[hit["path"]] += hit["occurrences"]
                    marker_rows.append({
                        "line": line_number,
                        "timestamp": timestamp or "",
                        "record_type": record_type or "",
                        "record_id": record_id or "",
                        "json_path": hit["path"],
                        "occurrences": hit["occurrences"],
                        "value_length": hit["value_length"],
                    })

            objects = usage_objects(record)
            if objects:
                summary["records_with_usage"] += 1
                summary["raw_usage_objects"] += len(objects)

                record_totals, unique_objects = choose_record_usage(objects)
                summary["unique_usage_objects_within_records"] += len(
                    unique_objects
                )

                for metric, value in record_totals.items():
                    summary[
                        "token_totals_after_in_record_exact_deduplication"
                    ][metric] += value

                marker_in_record = bool(hits)
                for item in objects:
                    usage_path_counts[item["path"]] += 1
                    metrics = item["metrics"]
                    usage_rows.append({
                        "line": line_number,
                        "timestamp": timestamp or "",
                        "record_type": record_type or "",
                        "record_id": record_id or "",
                        "marker_in_record": str(marker_in_record).lower(),
                        "json_path": item["path"],
                        "input_tokens": metrics.get("input_tokens", ""),
                        "output_tokens": metrics.get("output_tokens", ""),
                        "cache_creation_tokens": metrics.get(
                            "cache_creation_tokens", ""
                        ),
                        "cache_read_tokens": metrics.get(
                            "cache_read_tokens", ""
                        ),
                        "fingerprint": usage_fingerprint(metrics),
                    })

    summary["record_type_counts"] = dict(record_type_counts.most_common())
    summary["marker_path_counts"] = dict(marker_path_counts.most_common())
    summary["usage_path_counts"] = dict(usage_path_counts.most_common())

    if summary["malformed_lines"]:
        summary["warnings"].append(
            "Malformed lines were excluded from structured analysis."
        )
    if summary["raw_usage_objects"] != summary[
        "unique_usage_objects_within_records"
    ]:
        summary["warnings"].append(
            "At least one record contained duplicate usage metric sets; "
            "aggregate totals collapse exact duplicates only within that record."
        )
    summary["warnings"].append(
        "Identical usage values in different records are not deduplicated "
        "because they may represent separate API calls."
    )
    summary["warnings"].append(
        "Token totals describe logged usage fields, not a reconstruction of "
        "the model's exact context window."
    )

    with (output / "summary.json").open(
        "w", encoding="utf-8"
    ) as handle:
        json.dump(summary, handle, indent=2, ensure_ascii=False)
        handle.write("\n")

    def write_csv(name, rows, fields):
        with (output / name).open(
            "w", newline="", encoding="utf-8"
        ) as handle:
            writer = csv.DictWriter(handle, fieldnames=fields)
            writer.writeheader()
            writer.writerows(rows)

    write_csv(
        "marker-events.csv",
        marker_rows,
        [
            "line", "timestamp", "record_type", "record_id",
            "json_path", "occurrences", "value_length",
        ],
    )
    write_csv(
        "usage-events.csv",
        usage_rows,
        [
            "line", "timestamp", "record_type", "record_id",
            "marker_in_record", "json_path",
            "input_tokens", "output_tokens",
            "cache_creation_tokens", "cache_read_tokens",
            "fingerprint",
        ],
    )
    write_csv(
        "malformed-lines.csv",
        malformed_rows,
        ["line", "error", "text_length"],
    )

    totals = summary[
        "token_totals_after_in_record_exact_deduplication"
    ]
    print(f"Source: {source}")
    print(f"SHA-256: {summary['source']['sha256']}")
    print(f"Parsed records: {summary['parsed_records']}")
    print(f"Malformed lines: {summary['malformed_lines']}")
    print(f"Records with {MARKER}: {summary['records_with_marker']}")
    print(
        f"Literal {MARKER} occurrences: "
        f"{summary['literal_marker_occurrences']}"
    )
    print(f"Records with usage: {summary['records_with_usage']}")
    print("Token totals after within-record exact deduplication:")
    for key, value in totals.items():
        print(f"  {key}: {value}")
    print(f"Report directory: {output}")

if __name__ == "__main__":
    try:
        main()
    except BrokenPipeError:
        sys.exit(0)

Enter fullscreen mode Exit fullscreen mode

Run the audit

python3 audit_claude_session.py \
  "$AUDIT_DIR/session.jsonl" \
  --out "$AUDIT_DIR/report"

Enter fullscreen mode Exit fullscreen mode

      The output directory will contain:

Enter fullscreen mode Exit fullscreen mode

  • summary.json — source identity, counts, aggregate token values, paths, and warnings;

  • marker-events.csv — one row per JSON string containing ip_reminder;

  • usage-events.csv — one row per discovered usage object;

  • malformed-lines.csv — parse failures, without copying their potentially sensitive contents.

Inspect the summary

jq '{
  source,
  parsed_records,
  malformed_lines,
  records_with_marker,
  literal_marker_occurrences,
  records_with_usage,
  raw_usage_objects,
  unique_usage_objects_within_records,
  token_totals_after_in_record_exact_deduplication,
  warnings
}' "$AUDIT_DIR/report/summary.json"

Enter fullscreen mode Exit fullscreen mode

Inspect event distributions

      Count marker hits by JSON path:

Enter fullscreen mode Exit fullscreen mode

jq '.marker_path_counts' \
  "$AUDIT_DIR