Back to skill

Security audit

Chronicle

Security checks across malware telemetry and agentic risk

Overview

Chronicle appears to be a legitimate history tool, but it uses persistent broad capture of commands, prompts, files, transcripts, remotes, and model processing with enough scoping and redaction risk to require Review.

Install only if you intentionally want a persistent operational-history recorder. Pin the GitHub install to a reviewed commit, run hook installation with --dry-run first, avoid shell/git/remote hooks on sensitive machines unless you accept broad capture, keep ~/.chronicle, .chronicle, CHRONICLE.md, spines, and canvas private, and enable narration or remote sync only after reviewing the prompt/data that will leave the local machine.

Vulnerability Patterns
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (3)

T03 · Remote Payload Retrieval and Execution

Warning
Location
SKILL.md:21
Finding
Unpinned Remote Repository Is Retrieved and Executed During Installation<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:21-23` **Vulnerability Type**: Mutable remote payload retrieval and execution **Risk Level**: Medium ### Vulnerable Code ```markdown If the CLI is missing, install with `pipx install 'git+https://github.com/AntreasAntoniou/chronicle.git'`. Installing the skill does not install hooks or establish capture coverage. ``` ### Technical Analysis The documented installation command retrieves a Python package directly from the mutable default branch of a remote Git repository. It does not pin an immutable commit hash, verify a signed release, or validate an expected artifact digest. Installing a Python project through `pipx` executes package build and installation logic. Therefore, the code that is ultimately executed can change after this Skill has been reviewed without requiring any modification to `SKILL.md`. This is more specifically a remote payload retrieval issue than an ordinary dependency-version problem: the effective installation payload is fetched from an external URL and is not cryptographically bound to the reviewed source. ### Attack Path 1. An attacker compromises the referenced GitHub repository, a maintainer account, or the repository’s default branch. 2. The attacker modifies package source or build configuration on that branch. 3. A user or Agent follows the installation instruction in `SKILL.md`. 4. `pipx` clones the current repository state and invokes the Python packaging toolchain. 5. The modified code executes with the permissions of the user performing the installation. 6. The malicious package could then access user files or install additional hooks and persistent components. ### Impact Assessment Successful exploitation provides code execution under the installing user account. That scope can include access to the user’s projects, environment, credentials readable by that user, and configuration directories. The legitimate package also supports installing Agent, shel ...[truncated 322 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin installation to an immutable, reviewed commit: ```bash pipx install 'git+https://github.com/AntreasAntoniou/chronicle.git@<full-reviewed-commit-sha>' ``` 2. Prefer a versioned release artifact published through a trusted package registry. 3. Publish expected SHA-256 hashes for release artifacts and provide verification instructions. 4. Sign release tags and artifacts, and require signature verification before installation. 5. Avoid automatically updating from a mutable branch. 6. Document the exact package version or commit that corresponds to the audited Skill revision. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
src/chronicle/cli.py:233
Finding
Secret-Redaction Controls Can Be Bypassed Through Standard Input and Binary Content<![CDATA[ ## Vulnerability Details **File Location**: `src/chronicle/cli.py:233-255`; related persistence paths at `src/chronicle/capture.py:672-686` and `src/chronicle/capture.py:766-778` **Vulnerability Type**: Plaintext sensitive-data persistence caused by incomplete redaction coverage **Risk Level**: High ### Vulnerable Code The narrative credential guard checks the title and structured fields before reading stdin: ```python # Credential guard, inherited from v1: the record must never be the leak. blob = json.dumps(fields, ensure_ascii=False) + " " + args.title masked = cap.redact_text(blob) if masked != blob and not getattr(args, "allow_secretish", False): print(red("REFUSED: this entry looks like it carries a credential.")) print("Reference the config by path instead (e.g. 'see .env: SHOPIFY_TOKEN').") print("Override with --allow-secretish only if you are certain it is not one.") return 2 ev = { "kind": "narrative", "trigger": trigger, "entry": entry_id, "summary": args.title, "actor": {"kind": "agent" if os.environ.get("CLAUDE_SESSION_ID") else "human", "harness": os.environ.get("CHRONICLE_HARNESS", "cli"), "session": _session(), "model": os.environ.get("CHRONICLE_MODEL", "")}, "cwd": cwd, } ev.update(fields) git = cap.git_head(cwd) if git: ev["git"] = {k: v for k, v in git.items() if k != "root"} if getattr(args, "stdin", False) and not sys.stdin.isatty(): body = sys.stdin.read().strip() if body: ev["body"] = body ``` Binary files are stored without content redaction when their paths are not denied: ```python try: with open(path, "rb") as fh: data = fh.read() except OSError: desc["unreadable"] = True return desc if looks_binary(data): desc["binary"] = True else: red = redact_text(data.decode("utf-8", "replace")) data = red.encode("utf-8") try: desc["sha"] = cas_put(data) except Exception: desc["store_faile ...[truncated 3886 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Read stdin before running the credential check and include the complete body in the checked data. 2. Apply recursive redaction at `capture.emit()` so every string in every event passes through the same final safeguard before serialization. 3. Ensure the final redaction layer covers nested dictionaries, lists, command responses, narration output, and generic emitted JSON. 4. Reject or redact secret-shaped stdin by default rather than relying on callers to avoid it. 5. Do not store binary file contents by default. Store only size, hash, path, and capture status unless binary capture is explicitly enabled. 6. If binary recovery is required, require a per-project opt-in and use content-type-aware scanning before persistence. 7. Expand tests to verify that: - secrets supplied through `--stdin` never appear in lane or quarantine files; - nested event fields are redacted at the emission boundary; - non-denylisted binary fixtures containing credentials are not stored by default; - synchronization cannot publish unredacted event metadata. 8. Treat the existing filename denylist as defense in depth rather than the primary confidentiality control. ]]>

T02 · Agent Memory Poisoning

Error
Location
src/chronicle/narrate.py:176
Finding
Untrusted Trace and Transcript Content Can Poison Persistent Agent-Facing Narrative State<![CDATA[ ## Vulnerability Details **File Location**: `src/chronicle/narrate.py:176-220`, `src/chronicle/narrate.py:464-497`, and `src/chronicle/narrate.py:335-417` **Vulnerability Type**: Persistent memory poisoning through prompt injection into automated narration **Risk Level**: High ### Vulnerable Code Chronicle locates and reads the raw session transcript: ```python def find_transcript(session: str) -> Path | None: """Claude Code keeps per-session transcripts under ~/.claude/projects/**. Reading the transcript is what separates this narrator from one guessing at intent from tool calls alone: the transcript contains the reasoning that produced them. """ base = Path.home() / ".claude" / "projects" if not base.exists() or not session: return None for candidate in base.rglob(f"{session}.jsonl"): return candidate return None def _transcript_block(session: str | None, budget: int) -> str: if not session or budget <= 0: return "" path = find_transcript(session) if not path: return "" try: data = path.read_bytes() except OSError: return "" note = "" if len(data) > budget: half = budget // 2 dropped = len(data) - budget data = data[:half] + b"\n...[transcript truncated]...\n" + data[-half:] note = (f"\n[NOTE: {dropped:,} bytes of the middle of this transcript were " f"omitted to fit the context budget. Do not treat the gap as inactivity; " f"the EVENT TRACE above is complete and authoritative.]\n") text = cap.redact_text(data.decode("utf-8", "replace")) return ("--- SESSION TRANSCRIPT (the reasoning behind the trace) ---" + note + "\n" + text) ``` Captured events and the transcript are inserted directly into the model prompt: ```python prompt = PROMPT % { "schema": SCHEMA_HINT, "narrative": narrative_block, "n_events": len(window["events"]), "events": ...[truncated 4536 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Require explicit human review and confirmation before any model-generated entry is appended to the authoritative ledger. 2. Write proposed narration to a separate staging file or database table and provide approve, edit, and reject operations. 3. Use an API that supports separate system, instruction, and untrusted-data messages rather than passing one concatenated prompt through the CLI. 4. Clearly delimit and encode trace and transcript records as untrusted data, with explicit instructions that content inside those records must never be followed. 5. Parse transcripts into a minimal structured representation instead of forwarding raw transcript JSONL. 6. Add prompt-injection detection for instruction-like content originating from tool output, files, web responses, and transcripts. 7. Strengthen anchor validation by verifying that generated claims have lexical or semantic support in the cited events, not merely that the event IDs exist. 8. Reject generated imperative instructions and security-sensitive state claims unless independently confirmed. 9. Keep inferred narration out of default resume context unless the user explicitly requests it, or display it in a clearly separated untrusted section. 10. Disable unattended write-back from cron; scheduled narration should produce reviewable drafts only. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (35)

subprocess module call

Medium
Category
Dangerous Code Execution
Content
f'echo {payload} | base64 -d >> "$RC" && echo INSTALLED'
    )
    try:
        out = subprocess.run(
            ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=8", host, script],
            capture_output=True, text=True, timeout=45)
    except subprocess.SubprocessError as exc:
Confidence
88% confidence
Finding
Although subprocess.run is used safely on the local side, it passes a dynamically assembled shell script to a remote SSH shell, with the host target and remote script context not strongly constrained. In this installer context, that script appends persistent surveillance hooks to shell startup files on another machine, which makes the behavior materially security-relevant rather than a harmless subprocess use.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
return False

        local.write_text(json.dumps(merged, indent=2) + "\n")
        subprocess.run(
            ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=8", host,
             'mkdir -p ~/.claude && '
             '[ -f ~/.claude/settings.json ] && '
Confidence
85% confidence
Finding
This subprocess sends a remote shell command that creates and backs up agent configuration directories and files. While not a local shell-injection bug, it performs privileged remote configuration changes in a way that expands persistence and trust surface, which is dangerous in a skill whose purpose is ostensibly history preservation.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
# an identical re-run. Narration is a background convenience, so a transient blip
    # should cost a few seconds, not a session's story.
    for attempt in (1, 2):
        proc = subprocess.run(
            [claude, "-p", "--model", MODEL],
            input=prompt, capture_output=True, text=True, timeout=timeout)
        if proc.returncode == 0 and proc.stdout.strip():
Confidence
89% confidence
Finding
The code executes an external binary chosen from an environment variable (`CHRONICLE_CLAUDE_BIN`) or PATH without verifying its absolute path, ownership, or integrity. In the skill context, this narrator feeds large volumes of captured events and transcripts to that binary, so a malicious or trojaned executable could exfiltrate sensitive operational history and session contents or run arbitrary code under the user's privileges.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
# so a perfectly reachable machine that simply has no events yet exits 1 and gets
    # reported as unreachable. Conflating "nothing to report" with "host is down" is how a
    # fleet view becomes something you stop believing.
    probe = subprocess.run(
        ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=6", host,
         "test -d ~/.chronicle/lanes && echo lanes; "
         "test -d ~/.chronicle/cas && echo cas; true"],
Confidence
95% confidence
Finding
The SSH command passes an untrusted `host` value from `CHRONICLE_REMOTES` directly to the `ssh` client. Even though `subprocess.run` uses an argument list, `ssh` treats specially crafted host arguments beginning with `-` as options, which can alter behavior or inject a malicious `ProxyCommand`/config path and lead to command execution or unsafe connections.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
# ledger's reliability budget.
    codes = []
    if has_lanes:
        r = subprocess.run(
            ["rsync", "-az", "--timeout=60", "--include=*/", "--include=*.jsonl",
             "--exclude=*", f"{host}:.chronicle/lanes/", str(events) + "/"],
            capture_output=True, text=True)
Confidence
94% confidence
Finding
The rsync source argument embeds untrusted `host` data into `f"{host}:.chronicle/lanes/"` without validation. Rsync interprets this argument using its own remote-shell semantics, so a hostile value can be parsed as an option-like or alternate remote specification, causing unintended connections, option injection, or remote command abuse.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
capture_output=True, text=True)
        codes.append(("lanes", r))
    if has_cas:
        r = subprocess.run(
            ["rsync", "-az", "--timeout=120", "--ignore-existing",
             f"{host}:.chronicle/cas/", str(cas) + "/"],
            capture_output=True, text=True)
Confidence
94% confidence
Finding
This rsync invocation has the same untrusted host interpolation issue as the lanes sync path, here affecting CAS blob retrieval. Because this skill handles operational history and captured file content, abuse of the remote spec could redirect transfers, manipulate transport behavior, or exfiltrate sensitive archives from unexpected endpoints.

Tainted flow: 'claude' from os.environ.get (line 256, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Content
# an identical re-run. Narration is a background convenience, so a transient blip
    # should cost a few seconds, not a session's story.
    for attempt in (1, 2):
        proc = subprocess.run(
            [claude, "-p", "--model", MODEL],
            input=prompt, capture_output=True, text=True, timeout=timeout)
        if proc.returncode == 0 and proc.stdout.strip():
Confidence
96% confidence
Finding
`claude` is sourced from `os.environ` and then executed directly with `subprocess.run`, creating a tainted path to arbitrary code execution if an attacker can influence the environment. Because the process receives the full narration prompt on stdin, exploitation also gives the attacker access to potentially sensitive event traces and session transcript data, not just execution control.

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill advertises operational workflows but includes instructions that require shell execution, network access, package installation, and reading/writing local project state without declaring those capabilities. That mismatch reduces informed consent and can cause an agent or user to invoke a skill with broader authority than expected, especially since it suggests installing a remote package from GitHub.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The documented purpose is limited to preserving history, but the referenced behavior set extends into hook installation, telemetry capture, transcript harvesting, remote synchronization, background model invocation, and enforcement/blocking of shell operations. This is dangerous because it expands from note-taking into persistent surveillance and system modification, creating substantial risks of secret exfiltration, unauthorized persistence, and operational interference that a user would not reasonably infer from the description.

Context-Inappropriate Capability

Medium
Confidence
90% confidence
Finding
The code automatically creates .chronicle/.gitignore inside every visited repository, modifying repository contents without explicit user approval. Even though the goal is to hide trace artifacts from git status, silent writes into arbitrary repos can violate user expectations, interfere with tooling, and leave persistent artifacts in sensitive or read-only project trees.

Description-Behavior Mismatch

Medium
Confidence
80% confidence
Finding
This CLI exposes administrative capabilities beyond passive history preservation, including installing hooks and turning capture off/on. In a security-sensitive agent environment, those commands can reduce observability or expand persistence/integration surface, making the skill more dangerous than its stated purpose suggests.

Context-Inappropriate Capability

Medium
Confidence
78% confidence
Finding
The canvas command adds local web-serving capability that is not necessary for a minimal ledger CLI and increases attack surface. Even though the default bind host is 127.0.0.1, serving content over HTTP can expose sensitive chronicle data to local adversaries, browser-based attacks, or accidental broader exposure if host/port settings are changed.

Description-Behavior Mismatch

Medium
Confidence
98% confidence
Finding
The installer establishes persistent shell and git hooks that record every interactive command, commit subject, and changed file list, which materially exceeds a narrow 'preserve agent-session operational history' purpose. This creates broad, ongoing collection of user activity and repository metadata, including potentially sensitive commands and paths, without tight scoping to agent sessions.

Context-Inappropriate Capability

Medium
Confidence
96% confidence
Finding
The file includes SSH/SCP-based remote deployment and configuration of another machine, which is substantially broader than the stated need to preserve local session history. This capability can propagate persistent monitoring and configuration changes across hosts, increasing blast radius and creating an administrative-control channel unrelated to the skill's core function.

Context-Inappropriate Capability

Medium
Confidence
90% confidence
Finding
The narrator intentionally locates and reads full per-session Claude transcripts from `~/.claude/projects/**`, including reasoning and session text beyond minimal operational history. In this skill's context, that broadens collection from local chronology into potentially sensitive prompt, user input, and model-generated content, increasing exposure if transcripts contain secrets, private data, or unrelated context.

Description-Behavior Mismatch

Medium
Confidence
91% confidence
Finding
This feature sends local operational history to an external model-driven CLI for inference, expanding the skill from local recordkeeping into external processing. Even if intended, this creates a real data-exposure boundary and trust dependency on the external model/tooling that is more dangerous in a history-preservation skill because users may not expect their captured traces and transcripts to be reprocessed externally.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
User prompts are persisted to disk automatically, and prompts often contain proprietary code, credentials, customer data, or incident details. The file includes redaction for some token shapes, but there is no explicit consent or warning at the capture point, so users may disclose sensitive material they did not expect to be retained long-term.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
Bash command output is persisted, and command output commonly contains secrets, tokens, environment dumps, API responses, customer records, or decrypted material. The current redaction is pattern-based and incomplete, so sensitive data can be stored in the ledger and CAS despite the stated denylist protections.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The skill silently creates a .chronicle/.gitignore file in repositories with no explicit user-facing warning in this file. That is a persistent filesystem side effect in user projects, and in security-sensitive environments silent repo modification is itself risky and may breach policy.

Missing User Warnings

Medium
Confidence
97% confidence
Finding
The local shell hook installer appends code that captures every interactive shell command and working directory, but the install flow only reports that a zsh hook was appended and does not prominently warn at the moment of install that all future commands will be recorded. This undermines informed consent and can lead operators to enable persistent command surveillance without understanding its scope.

Missing User Warnings

High
Confidence
99% confidence
Finding
The remote shell hook installer silently enables persistent command capture on another machine by modifying that host's shell rc file. Because the warning is not surfaced as an explicit consent checkpoint during execution, this crosses into covert or insufficiently disclosed remote monitoring behavior with significant privacy and security implications.

Ssd 3

High
Confidence
98% confidence
Finding
The comments and event set explicitly describe recording user prompts, tool use, shell activity, and work history into a persistent ledger. In this context, that is not merely descriptive text: it reflects intentional collection and retention of sensitive operational data that may include secrets, commands, and user inputs beyond what is minimally necessary.

Ssd 3

High
Confidence
99% confidence
Finding
The zsh hook is specifically designed to record every interactive command together with cwd, exit code, duration, and session identifier. This creates a comprehensive command-history telemetry stream that can expose credentials, proprietary paths, operational procedures, and other sensitive user behavior.

Ssd 3

Medium
Confidence
95% confidence
Finding
The git hook records commit subjects and changed file lists into a persistent ledger. Even without full diffs, this metadata can reveal sensitive project names, file structures, incident response actions, security fixes, or confidential feature work across repositories.

Ssd 3

Medium
Confidence
94% confidence
Finding
The code explicitly uses full session transcripts as context because they contain the 'reasoning behind the trace', then includes that text in a prompt to an external model. This creates a direct plain-language leakage path for user prompts, operational details, and potentially secrets captured in transcripts, which is especially risky for a persistence/history skill that may aggregate broad cross-session context.

VirusTotal

VirusTotal findings are pending for this skill version.

View on VirusTotal

Static analysis

Detected: suspicious.exposed_secret_literal

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
src/chronicle/capture.py:1284

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
tests/test_capture_invariants.py:243