shellhist-core 0.1.0

Shell command-history reader: bash, zsh (extended), PowerShell PSReadLine, and fish history files → typed, timestamped entries
Documentation
# shellhist-core

[![shellhist-core](https://img.shields.io/crates/v/shellhist-core.svg?label=shellhist-core)](https://crates.io/crates/shellhist-core)
[![shellhist-forensic](https://img.shields.io/crates/v/shellhist-forensic.svg?label=shellhist-forensic)](https://crates.io/crates/shellhist-forensic)
[![Docs.rs](https://img.shields.io/docsrs/shellhist-core)](https://docs.rs/shellhist-core)
[![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE)
[![CI](https://github.com/SecurityRonin/shellhist-forensic/actions/workflows/ci.yml/badge.svg)](https://github.com/SecurityRonin/shellhist-forensic/actions)
[![Sponsor](https://img.shields.io/badge/sponsor-h4x0r-ea4aaa?logo=github-sponsors)](https://github.com/sponsors/h4x0r)

**A from-scratch shell command-history reader — bash, zsh (`EXTENDED_HISTORY`), fish, and PowerShell PSReadLine into one uniform, timestamped `HistoryEntry` stream. No `unsafe`, no regex engine, no C bindings — reads a history file authored on any OS.**

```toml
[dependencies]
shellhist-core = "0.1"
```

```rust
use shellhist_core::{parse_auto, detect, Shell};

// Detect the format from the bytes (the filename only disambiguates ties)…
assert_eq!(detect(b": 1700000000:0;ls", None), Shell::Zsh);
assert_eq!(detect(b"#1700000000\nls\n", None), Shell::Bash);

// …or just parse. zsh EXTENDED_HISTORY entries carry both timestamp and elapsed.
let entries = parse_auto(b": 1700000000:5;make build\n", None);
assert_eq!(entries[0].command, "make build");
assert_eq!(entries[0].timestamp, Some(1_700_000_000));
assert_eq!(entries[0].elapsed, Some(5));
```

## What it parses

`parse_auto(data, filename)` sniffs the format and parses; `detect(data, filename)` returns the [`Shell`] without committing; `parse(data, shell)` parses a known format. Per-format entry points live in `shellhist_core::{bash, zsh, fish, powershell}::parse`. Every path yields the same [`HistoryEntry`] (`shell`, `command`, `timestamp`, `elapsed`, `paths`):

- **bash** (`.bash_history`) — plain one-command-per-line, plus `#<epoch>` timestamp lines (`HISTTIMEFORMAT`) and multi-line commands that keep their embedded newlines.
- **zsh** (`.zsh_history`) — plain lines and `EXTENDED_HISTORY` `: <start>:<elapsed>;<cmd>` records with backslash continuation; both `timestamp` and `elapsed` are retained.
- **fish** (`fish_history`) — the nearly-YAML `- cmd:` / `when:` / `paths:` record format with its 2-rule unescape; heuristically associated paths are kept on the entry.
- **PowerShell PSReadLine** (`ConsoleHost_history.txt`) — plain commands with backtick line continuation; no timestamps (the format records none).

A leading UTF-8 BOM is stripped (`strip_bom`); an undetectable file parses as plain one-command-per-line (`Shell::Unknown`).

## Trust, but verify

`#![forbid(unsafe_code)]`; panic-free on crafted input (the workspace denies `clippy::unwrap_used` / `expect_used` in production code, parsing is lenient lossy-UTF-8 and bounds-checked); fuzzed with `cargo-fuzz` targets per format (`bash`, `zsh`, `fish`, `powershell`); the reader is exercised against a history file generated by a real `bash` subshell, not only synthetic fixtures.

## Forensic analysis

Severity-graded anomaly auditing (history-clearing / timestamp-regression / download-pipe-to-shell / encoded-PowerShell findings) lives in the sibling **[`shellhist-forensic`](https://crates.io/crates/shellhist-forensic)** crate, built on this one — the reader/analyzer split mirrors `ntfs-core`/`ntfs-forensic`.

---

[Privacy Policy](https://securityronin.github.io/shellhist-forensic/privacy/) · [Terms of Service](https://securityronin.github.io/shellhist-forensic/terms/) · © 2026 Security Ronin Ltd