What's New in hate_crack Since 2.0
What's New in hate_crack Since 2.0
Table of contents
- Getting the Update
- The Main Menu Was Renumbered
- Thirteen New Ways to Guess
- Two Old Attacks that Got Rebuilt
- Running the Cracker Backwards: the Spoonman Attack (22)
- Mining Your Own Logs: the Rosetta Attack (23)
- Somebody Else's 3.2 Million Cracks: Corporate Masks (24)
- The One That Reads Your Own Cracks: Smart Mask (25)
- Attack Coverage (85)
- The LLM Attack (12) was Rebuilt
- Wordlist Tools (80)
- Rule File Tools (81)
- Notifications (82)
- Hashview Integration (94)
- Non-Interactive Mode
- Rebuilding Your Results File (93)
- Things it Now Figures Out for You
- Sharp Edges, Filed Down
- Can You Trust the Tags?
- New Config Keys Since 2.0
Part 1 of 3. This post is the reference for the attacks and features. Part 2 (coming soon) is a deep dive into HashcatRosetta, the rule-analysis engine behind "Analyze Hashcat rules" in Rule File Tools, and how it was built. Part 3 (coming soon) is the plumbing: the config file split, how releases get cut, and the afternoon I pushed my private notes to a public repo.
TL;DR
If you're coming from 2.0 and you're not going to read the rest of this, read these:
• Re-run make
and re-init submodules so you actually have pcfg_cracker
and Corporate_Masks
. Three of the new attacks are dead without them.
• Train a PCFG grammar on a target-relevant corpus and use options 20 and 21. This is the biggest raw capability addition in the whole release range.
• After your first successful pass, run the LLM attack in cracked-password mode (12, then option 3) instead of only Loopback. It generalizes the organization's conventions instead of applying fixed rules.
• Once you have cracks, run Smart Mask (25). It's the cheapest new attack here and it finds the accounts sitting on a stem you already broke.
• Set HATE_CRACK_ARROW_MENU=1
and stop memorizing numbers, because they will move again.
• Turn on Notifications (82) so you stop babysitting idle terminals.
One thing I deliberately left out: "Analyze Hashcat rules," which is option 5 inside Rule File Tools (81) and was main-menu option 91 until 2.16.0. It's powered by a separate project of mine called HashcatRosetta, vendored here as a submodule and bumped several times since, most recently past v0.4.0 to pick up the hcmask parsing that Smart Mask leans on. It answers the question this reference can't, which is not "Which attacks exist?" but "Which of the rules you just ran actually did anything?" That's Part 2.
The numbering moved, and it will probably move again. If you land in Loopback when you meant Middle Combinator, that's the renumber catching up with your muscle memory, not a bug. Do a git pull
, glance at the menu, and set HATE_CRACK_ARROW_MENU=1
so you can stop counting.
Once I started using Claude regularly, I asked it if I was missing any useful attacks, then asked it to build out the ones I didn't have. A few of the new attacks came out of that. OMEN went in on February 17 as menu option 16. PassGPT went in the next day as option 17, a GPT-2 based password generator that fine-tuned on your cracks.
Then, I spent the rest of February 18 propping PassGPT up. Memory pre-checks, for training on large wordlists. Training time estimates, so you'd know what you were signing up for. Device auto-detection, because it defaulted to CUDA on machines that didn't have it. MPS fixes for Apple silicon. Five commits in one day, on a feature that was one day old.
On March 3, I deleted it, thirteen days after adding it. Not because it didn't work, but because I wasn't sure of what exactly what was happening with the data. PassGPT fine-tunes a model on the passwords you've already cracked, which, on an engagement, means client plaintext going into model weights on disk, through a stack of ML tooling that wasn’t audited. One of those five commits from February 18 was me disabling HuggingFace telemetry before the imports ran, which should tell you the sort of question I was starting to ask.
I sat down to vet whether the whole path was defensible from a DLP standpoint and could not convince myself that it was. So, I removed the feature before anyone really adopted it: all of it. I'd rather kill a feature I was proud of on day thirteen than find out on someone else's engagement that recovered client passwords went somewhere I didn't intend. Everything the current LLM attack does now runs against a local Ollama server for exactly that reason.
OMEN is still in there and it's good. It's option 13 now, because it just felt like the right thing to do.
hate_crack 2.0 landed in February 2026 and was mostly structural: the monolith got split into modules, Hashview got a real API client, and the test suite stopped being decorative. Since then, there have been 101 releases and 781 commits on main
. Thirteen new attacks, four new submenus, a rewritten LLM integration, scripted non-interactive mode, and a lot of sharper edges filed down.
This post is the reference for the attacks and features: what each one does, when to reach for it, how to invoke it, and the gotchas that go with it. If you're upgrading from 2.0 or anything in the 2.0-2.9 range, read the menu section first, because the numbering moved and it will bite you.
Configuration moved out of this post and into Part 3, and not because it got boring. Your credentials live in a .env
file now instead of config.json
, which is a breaking change, so if you're upgrading from anything before 2.20 go read that one too.
Getting the Update
Hashcat itself is no longer a submodule. Install it from your package manager or drop a binary somewhere and point hcatPath
at it. The supporting submodules (hashcat-utils, princeprocessor, pcfg_cracker, omen, HashcatRosetta, and Corporate_Masks) are all still submodules, so you still want a recursive clone.
For a fresh install, clone with submodules and run make
:
git clone --recurse-submodules https://github.com/trustedsec/hate_crack.git
cd hate_crack
make
make
auto-detects your OS, installs external deps (p7zip, transmission), builds the submodules (hashcat-utils, princeprocessor, pcfg_cracker, omen, HashcatRosetta, and Corporate_Masks), installs Python deps via uv
, and writes a bash shim to ~/.local/bin/hate_crack
. It's idempotent, so it skips anything already present. make reinstall
forces a clean pass.
Eventually, that shim replaced uv tool install
, which was unreliable about locating config and assets when run from an arbitrary working directory. For that reason, config resolution searches the repo root, the package directory, and ~/.hate_crack
, in that order, and it prints which files it landed on at startup. The whole story of how configuration resolves, and why your credentials moved to a second file, is in Part 3 (coming soon).
If you have an existing clone, git submodule update --init --recursive
will pull pcfg_cracker
, which is needed for two of the new attacks, and Corporate_Masks
, which is needed for a third.
For in-place upgrades, there's hate_crack --update
, an in-menu upgrade option, and an automatic startup version check controlled by check_for_updates
(default true
, runs async, network errors ignored).
The updater got rewritten around one specific failure: on a clone made before the master → main
rename, the local master
still tracks a ref that no longer exists, so git pull
reports success and changes nothing. A very confident no-op.
It's now built to survive whatever state your clone is in. It fetches from origin
before switching branches, checks out main
via git checkout -B main origin/main
regardless of local state, repairs the upstream config so your future manual git pull
works too, and pulls explicitly instead of trusting branch.*.merge
. Release tags live on main-side merge commits, so upgrading from a dev
branch switches to main
instead of no-op'ing. It refuses to clobber uncommitted work, surfaces a clear error when main
is checked out in another worktree, and leaves detached HEAD alone.
As of 2.17.0, there's also --nightly
, which updates from nightly-dev
instead of main
when you want something that's passed CI but hasn't been cut into a release. Plain --update
is unchanged and still tracks releases. The startup check only ever offers you releases either way, because it reads GitHub's "latest release" endpoint and nightly tags never populate it. Opting into the fast track is something you do on purpose, not something that happens to you.
You don't have to keep typing it, either. The update channel is a persistent setting now (update_channel
, default main
), so --nightly
and --no-nightly
are per-run overrides on top of whatever you configured.
The Main Menu Was Renumbered
This is the breaking change most likely to byte you (blame Andrew Lentz for that dad joke). Current menu:
(1) Quick Crack
(2) Extensive Pure_Hate Methodology Crack
(3) Brute Force Attack
(4) Top Mask Attack
(5) Fingerprint Attack
(6) Combinator Attacks
(7) Hybrid Attack
(8) Pathwell Top 100 Mask Brute Force Crack
(9) PRINCE Attack
(10) Bandrel Methodology
(11) Loopback Attack
(12) LLM Attack
(13) OMEN Attack
(14) Ad-hoc Mask Attack
(15) Markov Brute Force Attack
(16) N-gram Attack
(17) Permutation Attack
(18) Random Rules Attack
(19) Combipow Passphrase Attack
(20) PCFG Attack
(21) PRINCE-LING Attack
(22) Spoonman Attack
(23) Rosetta Attack
(24) Corporate Masks Brute Force
(25) Smart Mask Attack
(80) Wordlist Tools
(81) Rule File Tools
(82) Notifications
(85) Attack Coverage
(93) Regenerate .out from POT file
(94) Hashview API
(95) Analyze hashes with Pipal
(96) Export Output to Excel Format
(97) Display Cracked Hashes
(98) Display README
(99) Quit
Option 94 only appears when HASHVIEW_API_KEY
is set in .env
.
Attack Coverage is 85 and not 83 or 84 on purpose. Those two were notification toggles that got retired, and reusing a number somebody might still have in muscle memory is how you get a bug report that isn't a bug.
The important migration detail: YOLO, Middle, and Thorough Combinator used to be keys 10, 11, and 12. They're now inside the Combinator Attacks submenu at option 6. If you have muscle memory for 11, you'll get Loopback Attack instead. Set HATE_CRACK_ARROW_MENU=1
if you'd rather navigate with arrow keys than type numbers.
And if you're coming from anything before 2.16.0, notice what's missing from that block: the whole 90–93 range. The four download-and-analyze options that lived there were sitting at the top level for no better reason than that's where they'd accumulated, so 2.16.0 moved each one into the submenu it belonged in. The translation table:
Old key | Now |
90 Download rules from Hashmob.net | Rule File Tools (81) → 4 |
91 Analyze Hashcat Rules | Rule File Tools (81) → 5 |
92 Download wordlists from Hashmob.net | Wordlist Tools (80) → 9 |
93 Weakpass Wordlist Menu | Wordlist Tools (80) → 10 |
The --hashmob
, --weakpass
, and --rules
command-line flags are unchanged, so anything you've scripted still works.
That cleanup also caught a real bug hiding behind the duplication. hate_crack.py
kept its own copy of the main menu mapping, and in that copy key 91 was wired to the Weakpass menu, while both its own label and main.py
said "Analyze Hashcat Rules." So, if you reached the menu through the proxy path, 91 cheerfully ran the wrong handler. Two copies of the same table, disagreeing quietly, which is the entire argument against keeping two copies of the same table.
The Other Menu, Which the Consolidation Missed
There's a second menu you've probably seen without thinking about it: run hate_crack
with no hash file. This time, you get a short one instead of the full attack list, because most of the attacks have nothing to operate on. In 2.16.0 it looks like this:
(1) Hashview API
(2) Download wordlists from Weakpass
(3) Download wordlists from Hashmob.net
(4) Download rules from Hashmob.net
(5) Wordlist Tools
(6) Rule File Tools
(7) Exit
This is the consolidation only half-done. Options 2, 3, and 4 are the same three downloaders that 2.16.0 just finished moving into Wordlist Tools and Rule File Tools, sitting right above the two submenus that now also contain them. In the released version, you can reach the Hashmob wordlist download two ways from the same screen, which is precisely the state the whole change was meant to end.
That's fixed in 2.17.0. The three top-level copies are gone, and the menu is now 1 Hashview API, 2 Wordlist Tools, 3 Rule File Tools, 4 Exit. If you script against this, note that the --weakpass
, --hashmob
, and --rules
flags were never affected either way: they short-circuit before this menu is ever drawn.
Thirteen New Ways to Guess
PCFG Attack (20)
PCFG Attack (2) uses pcfg_cracker to generate candidates from a Probabilistic Context-Free Grammar, piping pcfg_guesser.py
straight into hashcat's stdin mode.
A PCFG models password structure, not just content. It learns that a password is often a baseword plus digits plus an optional symbol, learns the capitalization habits and keyboard walks that go with it, and attaches learned probabilities to each production. The practical consequence is that candidates come out in roughly descending likelihood order, so your early guesses are your best guesses. That's a different shape of attack from a wordlist plus rules, where quality depends entirely on how good your list and rule ordering happen to be.
Reach for it when you have a target-specific password corpus to train on: a previous engagement's cracks for the same client, or the plaintexts you've already recovered this run. Against a generic grammar it's respectable. But, against one trained on the target's own passwords? It's very good.
This requires the pcfg_cracker
submodule. Presence is checked at startup and reported non-fatally, so if you skipped make
the PCFG options are simply unavailable instead of throwing a traceback at you.
Config keys:
pcfgRuleset
(defaultDefault
): name of the trained grammar, resolved topcfg_cracker/Rules//
. The default used to beDEFAULT
, which is not a directory that exists; 2.14.8 fixed the default and made the lookup case-insensitive, and 2.15.1 fixed the case-insensitive lookup so it actually waspcfgMaxCandidates
(default50000000
): candidate cap
I raise this one to 100000000
as well, for the same reason as OMEN. pcfg_guesser.py
is Python rather than compiled C, so it's slower than enumNG
, but not by as much as you'd expect: it sustains around 770,000 candidates a second once startup amortizes. Fifty million takes 64 seconds and a hundred million takes 130. Two minutes of generation to double your candidate space is a trade worth making on anything but a throwaway run.
Those numbers used to assume you were running from an actual terminal, and if you weren't, the cap you set was fiction. That's fixed as of 2.14.8, and it's covered below in the section on the four edges that used to still be sharp. Make sure you're on 2.14.8 or later and the number you configure is the number you get.
One limitation up front: hate_crack does not wrap grammar training. To build a grammar from a target-specific set, run pcfg_cracker's own trainer.py
and point pcfgRuleset
at the resulting ruleset name.
PRINCE-LING Attack (21)
PRINCE-LING Attack (21) uses pcfg_cracker's prince_ling.py
to derive a PRINCE base wordlist from a trained grammar, then hands off to the existing PRINCE attack.
PRINCE builds candidates by combining words from a base list, which means the quality of the base list dominates everything. Point PRINCE at a generic wordlist and most of the combination space is garbage. PRINCE-LING asks the grammar which base words are actually productive and builds the list from those, so the same PRINCE machinery spends its time on combinations that have a real chance.
The generated wordlist is cached at /pcfg_prince_ling__.txt
and reused across sessions. The size is in that filename for a reason: without it, raising pcfgPrinceLingMaxCandidates
hit a cache keyed only on the ruleset name and the new cap never took effect. You’ve got the old wordlist and no indication you'd been handed one. Old caches from before the rename are ignored rather than migrated, so the first run after upgrading regenerates. Regeneration is triggered only when the ruleset directory's mtime is newer than the cache's, so retraining a grammar invalidates the cache automatically and nothing else does. Generation writes to a temp file and atomically moves it into place; a failed or Ctrl-C'd run cleans up the partial file and leaves any existing cache intact.
Capped by pcfgPrinceLingMaxCandidates
(default 10000000
).
OMEN Attack (13)
Ordered Markov ENumerator (OMEN) trains a statistical model from a wordlist and enumerates candidates in probability order. Conceptually adjacent to PCFG, since both are "learn the distribution, emit best-first," but OMEN models character transitions instead of password structure.
It needs the OMEN binaries (createNG
and enumNG
) built from the omen submodule. The interactive flow gives you use-existing-model, train-new-model, or cancel, with a training wordlist picker that lists your configured wordlist directory or takes a custom path. Rules are supported.
Model files and metadata persist in ~/.hate_crack/omen/
so you're not retraining every session.
It validates all five required model files (createConfig
, plus the CP/IP/EP/LN.level
files) before launching, and surfaces enumNG
stderr instead of swallowing it. An incomplete model is otherwise a very easy thing to not notice until a long run has produced nothing at all.
Config key: omenMaxCandidates
(default 100000000
). There used to be an omenTrainingList
alongside it, and it's gone. Not deprecated, deleted. It was documented, settable, and read by absolutely nothing. Part 3 covers the test that went looking for keys with no read site and found three of them.
That cap is worth understanding, because it's the only thing deciding how long the attack actually runs. It feeds straight through to enumNG -p -m
. On my box enumNG
spends about 1.4 seconds loading its model and then emits candidates at roughly 1.2 million a second:
| Generation time |
| 2.4s |
| 9.6s |
| 40.5s |
| 80.3s |
Eighty seconds of generation is a rounding error inside a real session, and two things make a cap that high productive rather than just slow. The candidates are unique, so you aren't paying for duplicates: a million-candidate run dedupes to exactly a million lines. The probability ordering holds up that deep, so you aren't buying noise either. At the hundred-million mark, it's still emitting things like Bellissie2019
and Santon789012
, which are perfectly plausible passwords.
It didn't always default this high, and the fix is new enough that it's worth being specific. Two different numbers were in play. config.json.example
shipped 50000000
, but the in-code fallback that applies when your config.json
omits the key entirely was 1000000
. So, if you copied a config from an older version, or hand-wrote one, OMEN finished generating in under two and a half seconds and looked like it simply hadn't done anything. 2.14.4 aligns both at 100000000
. If you're on anything older, this is the one config value to go check.
The constraint underneath all of this is that enumNG
is single-threaded CPU generation piped into hashcat. Against a fast hash your GPU will eat 1.2 million candidates a second without noticing, so generation is the bottleneck, not cracking.
Markov Brute-Force Attack (15)
This generates candidates from hashcat's own .hcstat2
Markov tables. Simpler and faster than OMEN, and the one to reach for when you want statistical brute force without a training ceremony.
The flow checks for an existing .hcstat2
table from a previous session and offers reuse, regenerate, or cancel. If it needs to build one, training data comes from either the cracked passwords in the current session's .out
file or any wordlist you pick. Then, you choose min and max password length, and it runs with --increment
so lengths are tested in sequence.
The table persists next to the hash file as .hcstat2
, so subsequent runs against the same target start immediately.
Training on the current session's cracks is the high-value path here: you're brute-forcing in the shape of passwords this organization actually uses instead of in the shape of the Internet's.
N-gram Attack (16)
N-gram Attack (16) generates n-gram candidates from a corpus file using ngramX.bin
, piped into hashcat.
Prompts for a corpus file with tab completion (defaulting to your wordlist directory) and an n-gram group size (default 3). Gzip-compressed corpora are auto-detected and decompressed on the fly.
The use-case that makes this worth having is this: you have target-relevant prose instead of a password list. Scraped marketing copy, a leaked document set, an internal Wiki export, and product documentation. Feed it the corpus, and it produces the character-level sequences that appear in the target's own language, which is where their invented basewords tend to come from.
Permutation Attack (17)
This generates every character permutation of each word in a wordlist via permute.bin
and pipes them to hashcat. Takes a single wordlist file, not a directory.
This is for short, targeted, high-confidence wordlists where you know the character set but not the order: company abbreviations, name fragments, known tokens. It scales as N! per word, so an 8-character word produces 40,320 permutations and a 10-character word produces 3.6 million. Keep inputs to roughly 8 characters or shorter and keep the list small.
Random Rules Attack (18)
This attack generates a set of random hashcat mutation rules with generate-rules.bin
, writes them to a temp file, and runs hashcat against a chosen wordlist with those rules. Prompts for rule count (default 65536
) and a wordlist, with tab completion and numbered selection. The temp rules file is cleaned up afterward regardless of outcome.
This is the "I've exhausted the known rule sets" attack. When best64, d3ad0ne, OneRule, and the Hashmob collection have all stopped producing, random rule-space exploration will sometimes still find cracks. It's a lottery ticket, but it's a cheap one, and it's a reasonable thing to leave running overnight after your methodical passes are done.
Combipow Passphrase Attack (19)
This generates all unique non-empty subset combinations from a short wordlist using combipow.bin
and pipes them into hashcat. Built for passphrase cracking when you know the pool of words a passphrase was built from but not which ones were used or in what order.
- Hard limit of 63 lines, because combipow generates up to 2^n-1 combinations. Over 63 it aborts with a clear message.
- Warns above 20 lines, where output volume starts getting serious.
- Optional space separator (
-s
) to insert spaces between words in each combination, which matters for real passphrases.
Ad-hoc Mask Attack (14)
An Ad-hoc Mask Attack runs a hashcat mask attack (-a 3
) with a mask you type at the prompt, with optional custom charsets via -1 through -4. Charset entry is interactive and exits early on blank input.
It’s nothing clever, and that's the point. Running one arbitrary mask no longer means dropping out to invoke hashcat by hand and giving up the session naming and .out
file conventions the rest of the tool relies on.
There are two additions. hashcat 7 raised the custom charset count from four to eight, so ?5 through ?8 is offered when the installed hashcat is 7 or newer and is warned about when it isn't. Additionally, there's an incremental mode: give it an increment min and max or leave both blank to pass a bare --increment
.
It takes a mask file now too. Pick option 2 at the prompt and you get a tab-completing path picker rooted at masks/
, which is where the Pathwell and PACK output already lives. The custom-charset prompts are skipped on that path, because mask files define their own charsets inline and hashcat takes a mask file in the same argument slot as a literal mask. So running a whole .hcmask
file through the normal session plumbing costs one keystroke.
Combinator Attacks Submenu (6)
Four entries, consolidating what used to be scattered across keys 10-12:
- Combinator Attack (2-8 wordlists): the general case
- YOLO Combinator: all permutations of multiple wordlists
- Middle Combinator: combines wordlists with an extra word in the middle
- Thorough Combinator: comprehensive combination with rules applied
That first entry is the one that changed shape. It used to be three separate menu items (plain Combinator, Combinator3, and CombinatorX) and picking the right one was your job. Now you give it two to eight wordlists and an optional separator, and it dispatches: two lists and no separator use hashcat's native combinator mode, three and no separator use combinator3.bin
, and anything else goes to combinatorX.bin
with --sepFill
. Same three code paths, one decision fewer for you to get wrong.
The separator is the part worth knowing about. It handles the Word-Word-Word
and Word.Word.Word
patterns that show up constantly in passphrase-ish corporate passwords, which plain concatenation misses entirely.
Two Old Attacks that Got Rebuilt
Neither of these is new, so neither counts toward the thirteen. Both changed enough that your notes on them are wrong.
The Fingerprint Attack (5) Now Does What The Wiki Says
This one was quietly not the attack it claimed to be. The hashcat Wiki describes fingerprint as combining expander output with itself or with dictionary entries, escalating from small dictionaries to big ones. What hate_crack did was self-combine a single fixed expander length, re-expand the entire cracked corpus on every convergence-loop iteration, and put no bound at all on the resulting -a 1
keyspace, which is O(n²) in fragment count.
Four things changed. It escalates through an expander-length chain, 7 then 14 then 21 and onward up to a ceiling that defaults to 21, running each length to convergence before advancing, which is why expander_len
is now max_expander_len
. Each iteration expands only the newly cracked plaintexts and merges into the accumulating .expanded
file instead of re-running the expander over the whole growing corpus. An optional dictionary wordlist (hcatFingerprintWordlist
, default empty) combines fragments against a real list in both directions, and every -a 1
combination carries a cheap -j c -k c
capitalize rule on both sides for free capitalized-variant coverage.
One more landed after that, and in the same spirit as the second: the secondary hybrid pass used to run on every inner-loop iteration, against a still-growing .expanded
file. It runs once per expander length now, after the expand-and-combine loop has stopped finding new cracks on its own, so hybrid always sees the fully-expanded fragment set for that length instead of a partial one.
The fourth is the one worth stealing. There's a keyspace guardrail now, and it skips an over-threshold combination with a warning rather than asking you about it. Fingerprint gets launched and then left alone for hours, both as a standalone menu item and inside the Extensive chain, so a y/N
prompt in the middle of it has nobody there to answer and just hangs the attack. That got reported live, with hashcat idle and the next step sitting on stdin. The standalone version asks for the threshold up front instead, before hashcat starts, defaulting to 50 billion candidates with 0 for no limit. A question you can't answer isn't a safety feature.
The Hybrid Attack (7) Got Wider and Grew a Clock
The old passes only ever tried ?s?d
, which means a decoration containing a letter was out of reach entirely. Masks now run lengths 1 through 4 in each direction rather than 2 through 4, so the cheapest possible pass, a single appended or prepended character, finally exists. And there's a full ?a
sweep at lengths 1 through 4 both ways on top of the ?s?d
one.
?a
is every printable character, so the new group is a strict superset of the old: 95⁴ instead of 43⁴ at the longest mask, roughly 1.2e15 candidates over rockyou.txt. That's about ten hours for NTLM at 32 GH/s and completely hopeless on a slow hash. So, the cheap ?s?d group runs first and gets its shot before twenty-four times the work repeats it, and the whole attack is bounded by hcatHybridMaxRuntime
(seconds, default 3600
, 0 for unlimited). It’s time-bounded rather than length-bounded, because the cost here is wordlist times mask, and a small targeted list finishes every pass outright.
Running the Cracker Backwards: the Spoonman Attack (22)
Everything else in this post ships in v2.16.0. This one missed that cut by about four hours and went out in v2.17.0 the same afternoon, which tells you how badly I wanted it in. It's the most interesting idea anyone has handed me in a while, and it came from the one person with standing to hand it to me: @Spoonman1091 is Larry Spohn, whose name has been in the header of this script since he wrote the original version of it. He filed it as issue #169 with a working rulegen.py
attached, which is a considerably better bug report than most feature requests. I've spent this whole post narrating things I changed about Larry's tool. Nice to get a turn where he tells me what to add to it.
Every other attack in this post generates candidates and hopes. This one runs the process backwards. You give it a corpus of passwords you already know (a previous engagement's cracked output, a leak dump, whatever plaintext you've got) and it derives the baseword list and the rule file that reconstruct that corpus. Not approximately. The baseword × rule cross product rebuilds every password in the input, exactly.
The trick is that a password is just a baseword plus a transformation, and if you have the password you can solve for the transformation. Each entry gets split into its letters-only lowercased core, and then a rule is emitted that rebuilds the original from that core: l/u/c
for the casing, T{p}
to toggle a specific position, ${x}
and ^{x}
for trailing and leading characters, and i{p}{x}
for the ones buried in the middle. Summer2024!
comes apart into summer
plus the rule that puts the capital, the year, and the bang back.
By itself that's a party trick, because a rule file that reconstructs your old corpus exactly is a rule file tuned to passwords you've already cracked. What makes it an attack is the ordering. Rules are sorted by how many passwords in the corpus each one rebuilds, most productive first, which makes the file truncatable, and the head of it is a ranked, empirical model of how this population actually mangles words.
The menu offers five sizes: top 50%, 75%, 95%, 99%, or the full set. Reach for top 50%.
Then, it asks a second question, which is newer and matters more than the first one. Where should the basewords come from: the derived ones only, the derived ones plus your configured wordlists, or the derived ones capped to the most frequent N. Take the second one, and the measurement is why. I went looking at what this attack actually misses, and it's baseword-limited, not rule-limited. Depending on the corpus, 47 to 57 percent of the misses are passwords where no baseword in the file could ever have produced them, against only 18 to 21 percent where no rule could. The rule half of this attack is in good shape. The word half was starving.
You also get offered the current session's own .out
as the corpus, ahead of the free-form path prompt, whenever there's anything in it.
I originally shipped this with only the full set, 99%, and 95%, and those numbers were all wrong in the same direction. Coverage against a real corpus is violently long-tailed. On a 98.2M-password sample, 50% coverage needed 4,120 rules. 95% needed 16,119,661. The full set needed 21,029,696. Every option I'd offered sat past the knee of that curve, which means the "conservative" choice was still handing you sixteen million rules and calling it a trim. Four thousand rules that rebuild half the corpus is a rule file you can actually run.
Output is cached beside the hash file at .spoonman
as basewords.txt, rules.full.rule
, the capped variants, a coverage.txt
recording how many rules each milestone needed, and a corpus.json
holding the corpus's absolute path, size, and mtime. That provenance record is what drives invalidation, so derivation only re-runs if the corpus actually changed and pointing it at the same list twice is free.
Two details in the implementation are worth calling out, because they're the difference between a tool and a demo.
hashcat silently drops over-long rules. A rule can hold at most 31 functions (verified in the code's own comment: 31 run, 32 yields "No valid rules left"). A long or weird password can need more than that to reconstruct. The nasty part is what hashcat does about it: if the offending rule shares a file with valid ones, it discards that rule without a word and carries on. So, the naive version of this tool reports 100% coverage and delivers less, which is the exact failure mode I spent half this post complaining about in other features. Instead, any password that can't be expressed inside the limit is written out verbatim as its own baseword with a : no-op, so the coverage number stays honest. Same handling for hashcat's other ceiling, though that one turned out to be softer than I'd claimed: positions are encoded in a 36-character alphabet, so i can't address past index 35 counting from the left. But r reverses the word, and a break in the last 36 characters is addressable once reversed, so the line-break path inserts it there and reverses back. Case encoding doesn't get the trick, because a reversal would have to compose with every other op in the derivation instead of standing alone as the whole rule, so an uppercase letter past index 35 still falls back.
That accounting got split since, because two very different things were landing in that fallback bucket and only one of them is a loss. A password with no ASCII letter in it at all has no letters-only core to derive from, so being its own baseword is the correct answer rather than a defect; those count as no_letter_literals
. A password that does have letters and still couldn't be encoded is a real loss of expressiveness, and those count as unrepresentable
. On a 360,000-password sample, the split was 100% the first kind and zero of the second, which is the useful finding: the hashcat limits are real, but on the six-to-twelve-character passwords a corpus is actually made of, they essentially never fire.
There's a third counter, unwritable_basewords
, and it's the one I'd rather not need. A baseword containing a byte the wordlist file has no way to hold gets skipped and counted instead of written. Writing it would split its own record across two lines and hand hashcat two wrong words, silently, and the reconstruction self-check can't catch that because the derivation is correct right up until the writer destroys it. Skipping and counting is worse coverage and better arithmetic. More on that one below.
It checks its own arithmetic. The derivation reconstructs every password in-process and reports failures rather than asserting success. Which is the correct instinct, and if you've read Part 2 (coming soon), then you know it's the same one that took me an embarrassingly long time to arrive at on my own project.
Mining Your Own Logs: the Rosetta Attack (23)
Spoonman learns from passwords you already cracked. Rosetta learns from the attacks you already ran.
Every rule-based attack in hate_crack now writes a hashcat debug log without being asked, which records, for each candidate, the baseword it came from and the rule that produced it. When a candidate cracks a hash, that log line is a receipt: this baseword plus this rule beat this target. The Rosetta Attack reads those receipts back.
The insight is what it does with them, and it isn't "retry the pairs." A pair that appears in a log already cracked its hash, so replaying it gains you nothing. What's valuable is the cross product. A rule that worked on one baseword has usually never been tried against the others, and a baseword that cracked under one rule has usually never seen the rest. So, it takes the winning basewords and the winning rules, crosses them, and runs the result as an ordinary dictionary-plus-rules attack.
Reach for it once you've been grinding a target for a while and have a pile of logs. It's not an opener. With no cracks, there are no receipts, and it has nothing to work with.
The flow asks how to rank the rules first, which is the same three metrics Part 2 is about:
(1) Application frequency (rules applied most often)
(2) Baseword spread (rules that worked across the most basewords)
(3) Candidate variety (rules producing the most unique candidates)
(4) LLM Mask Attack (natural language -> hcmask)
That fourth one isn't a ranking metric and isn't the debug-log half of anything. It's a separate attack that happens to live here. You describe the passwords you expect in plain English, a local model turns that into hashcat masks, each one gets deterministically validated before you see it, and the result runs as -a 3
out of .hcmask
. It's the only entry on this menu that doesn't read your logs.
For the first three, you then pick logs — up to 20 from your debug directory, newest first, zero-byte logs filtered out, and you can take all of them or type a path — and get asked how many top rules to keep and how many basewords. The menu comes before the log selection on purpose, so you're not choosing logs for an attack you then back out of. The rule default is now all, not 100. That changed because the old default quietly discarded every rule below the hundredth, which on a decent pile of logs is most of them. Type a number if you want a cap; 0 also means all.
One implementation detail matters to you: derived output lands in .rosetta/
as basewords.txt
and rules.rule
, re-derived every run. Unlike Spoonman, there's no cache to invalidate, which also means an old rules.rule
never gets repaired in place. If you generated one on an older version, re-run the attack rather than reusing the file.
The million-line read cap this post used to warn you about is gone. Every line of every log you select gets read now.
This is also the one place in hate_crack that imports HashcatRosetta for something other than printing a table, which is why Part 2 exists.
Two related changes came in alongside it. Debug logs default to ~/.hate_crack/hashcat_debug
instead of a path relative to wherever you launched from, because the old default wrote cracked plaintext into whatever directory you happened to be in and scattered the logs this attack wants to read. And the logs are --debug-mode 5
now instead of mode 4, which appends the source wordlist as a fourth field, so a multi-wordlist run records which list earned its keep. Mode-4 logs from before the switch still parse and they get normalized per file.
Somebody Else's 3.2 Million Cracks: Corporate Masks (24)
The Pathwell masks at option 8 are the ones everybody has. They're a good general-purpose set and they're also a decade of general internet passwords, which is not the same population as the one you're usually pointed at.
Option 24 runs statistical eight to fourteen character masks derived from 3.2 million NTLM hashes cracked on real engagements, from Corporate_Masks by golem445, which is a new submodule. You give it a min and max length, and it runs each length as a separate hashcat invocation. Missing mask files are handled rather than thrown at you, and it respects optimizedKernelAttacks
like everything else.
The pitch is just that the prior is better. Corporate password policies produce a distinctive shape, and masks built from corporate cracks spend their keyspace where corporate passwords actually live.
The One That Reads Your Own Cracks: Smart Mask (25)
This is my favorite of the new ones, because it finds a thing you would never have thought to brute-force.
Take the passwords you've already cracked this session and look for a literal skeleton shared by three or more of them: a fixed stem with a short varying run stuck on it. ChangeMe2024
, ChangeMe2025
, and ChangeMe1624
. Once you can see that shape, the interesting question isn't the three accounts you already have. It's the other accounts in the domain sitting on the same stem with a number you haven't guessed.
Config keys: hcatSmartMaskMinClusterSize
(default 3) sets how many passwords have to share a skeleton before it counts as a pattern.
So, it collapses each pattern into a targeted attack. Templates whose variation sits at one end of the password become hybrid runs, one -a 6
or -a 7
invocation per distinct mask, with every template's literal stem as a line in that run's wordlist. Whatever's left, meaning variation in the middle, stays -a 3
in a single .hcmask
file with its charsets widened, since it didn't get the benefit of the collapse. You get asked for a per-pattern keyspace guardrail, defaulting to 50 billion, and 0 disables it.
That collapse isn't a tidiness thing, and the reason is a good lesson about hashcat. It treats every line of a .hcmask
file as an independent attack: it autotunes per line and applies the base-word check per line. Every Smart Mask line was a fixed stem plus two to four varying characters, so each one was ten thousand candidates or fewer against a device asking for millions of base words, and hashcat complained on stderr every single time. On a representative corpus, that was 314 warnings out of 314 lines.
The obvious fix doesn't work either. You can't merge the lines, because a mask line's literal is its identity, so two lines with different stems have completely disjoint candidate sets. Measured across those 314: zero exact duplicates and zero subsumptions. And a single bigger mask doesn't help, because a ten-million-candidate -a 3
still warns. The only way out was changing attack mode.
Three bugs from building this are worth stealing, because all three are the same species: silent losses inside a file format.
A pattern whose mask line started with # was never tried. Hashcat reads that as a comment, every other line in the file still runs, and the exit status is 0. Nothing anywhere tells you a pattern vanished. Two routes reached it: a custom charset containing #, which sorts ahead of nearly everything and therefore led the line, and a stem that simply starts with #. The second is worse than a comment, because with no charset field present hashcat rejects the whole file with "Invalid mask" and the attack tries nothing. A 60-template fuzz against hashcat --stdout
went from 13 rejected mask files to zero.
A ? inside a custom charset definition needed escaping, since mp_expand
tokenizes the definition itself, not just the mask. In a file that cost one line. On argv, where the collapsed hybrid groups pass charsets with -1
, hashcat exits 255 having tried nothing, which loses every stem in the group.
It was emitting 0123456789
as a custom charset and burning a ?1
slot on it, where ?d
enumerates the identical candidates in a shorter line and costs no slot at all. Charsets now get matched against hashcat's eight built-ins by set equality first, which, on a representative cluster set, took the emitted file from 225 bytes to 141 and stopped any line needing more than one custom slot.
Attack Coverage (85)
This isn’t an attack, but it changes how you run them.
On a long engagement, you lose track. You've run best64 against four wordlists over three days and you genuinely cannot figure out which combinations you’ve tried. So, you either re-run it and waste the GPU time or skip it and hope. hate_crack now records which rule lines, hcmask lines, and wordlists have already run against a given hash file, in a SQLite store at ~/.hate_crack/coverage/attack_coverage.sqlite3
, and offers to skip the overlap.
The submenu at 85
is 1
show coverage, 2
run history, 3
forget all, 99
back. There's a matching CLI tree, hate_crack coverage status|history|forget --hashfile X
, with forget
asking for confirmation unless you pass --yes
. Both surfaces render through the same helpers so they can't drift apart.
The hash file is identified by content rather than path, so all of this survives you moving or renaming it, which you will on a real engagement.
Two things to know. First, coverage_enabled
defaults to true and --no-coverage
turns it off for a run. Second, in the non-interactive subcommands, a run skipped because it was already covered exits 3
when you pass --exit-code-on-skip
, so a script can tell "already done" apart from "worked" and from "broke."
There was one specific case where I'd have told you not to trust it blindly, and it's in the sharp-edges section below. It’s fixed now, but worth reading if you're on an older build.
The LLM Attack (12) was Rebuilt
This is the most heavily reworked part of the codebase, and it's worth a fresh look even if you've used it before.
Candidate generation lives in hate_crack/llm.py
and goes through the Atomic Agents framework for structured JSON output, so candidates arrive as validated objects instead of text to be scraped. The default model is qwen3:4b-instruct
. It used to be qwen2.5:32b
, picked for reliable schema adherence rather than raw capability, and that was the wrong trade: 32B needs roughly 20 GB of VRAM to run at a usable speed, which put the whole attack out of reach on machines that run the rest of hate_crack fine. An existing .env
still overrides it, so only a fresh install sees the new default. If you do override it, pick something good at structured output or you'll be debugging validation retries.
hate_crack no longer auto-pulls missing models. Run ollama pull qwen3:4b-instruct
yourself.
Four Generation Modes
1. Target info. You supply company, industry, location, and parent company; the model derives candidates from those details using industry terminology and company name permutations. The prompt uses a CTF-style framing.
This mode gains an automatic research step. Once you type the company name, hate_crack asks the same local model what it knows about that organization and pre-fills the remaining prompts as editable defaults:
Company name: Acme Rail Services
[!] The values in parentheses below are the local model's GUESSES, not verified OSINT.
Press Enter to accept, or type your own value to override.
Industry (freight rail maintenance):
Location (Omaha, Nebraska):
Parent company / acquired by (Union Pacific):
Enter accepts, typing overrides. Treat those values as a starting point and nothing more. They're model recollection, not intelligence about your client, and the warning text says so on purpose. Values are whitespace-collapsed and capped at 80 characters.
The research call runs entirely against your local Ollama server. There are no web calls and no third-party APIs, so the client name never leaves the host. If the model doesn't recognize the organization, which is the common case for small clients, it returns nothing and you get plain blank prompts. Any failure or timeout falls back to blank prompts and never blocks the attack. Set OLLAMA_AUTO_RESEARCH=0
in .env
to skip it entirely, which is worth doing if your model is slow, since research costs an extra round-trip before the attack starts. It also recalls parent company and acquisition history now, alongside industry and location, which for a client that got bought is often where the actual basewords come from.
2. Wordlist. This derives basewords from a sample wordlist. Useful for building a client-specific denylist, or for extracting the useful core out of a list that's mostly noise.
3. Cracked passwords. This feeds the plaintexts you've already recovered this session (.out
) back to the model so it can infer the organization's own password conventions (their basewords, their season-and-year habits, their suffix and leetspeak patterns) and generate new candidates in that style. The prompt explicitly instructs the model not to re-emit passwords you already have. This option is only listed once at least one hash has been cracked.
Mode 3 is the one that changes your workflow. It's conceptually the Loopback Attack, except the loop runs through something that can recognize "everything here ends in the fiscal quarter" and generalize from it, rather than applying a fixed rule set.
4. Pattern rules. Theis is the newest and most interesting one. Instead of asking the model for finished candidates, this asks it for the two halves of a rule-based attack: a baseword list and a hashcat rule file. Then it runs them against each other.
It's the Spoonman Attack with the exactness traded for imagination. Spoonman extracts basewords and rules that reconstruct your corpus literally, which is why the coverage number is honest and why it can't produce anything the corpus didn't already contain. This mode asks a model to name the word families behind the corpus (company and product names, sites and departments, local sports teams, mascots, seasons, and keyboard walks) and enumerate more members than you actually cracked, then separately asks it to reproduce the corpus's decoration habits as raw hashcat rules. It generalizes where Spoonman can only reflect.
You get asked for a source (the current session's cracks if you have them, otherwise a wordlist) and that's it. You are not prompted for a rule file, because the model writes that too, which is the whole point of the mode.
The guardrails are worth knowing, because "ask a model for hashcat rules" invites exactly the failure you're imagining. Every generated rule goes through the same validator the Spoonman rule generator uses, and duplicates are dropped. If fewer than 25 valid rules survive, it asks again, once. If nothing valid comes back at all, the attack runs the basewords bare rather than aborting. Basewords get lowercased, stripped to letters, and discarded under three characters. Output lands in .llm_patterns/
.
One note on mode 4 and mode 3: the option is always numbered 4
, whether or not 3
was offered. Renumbering it per session depending on whether you've cracked anything would move the option under you between runs, which is the exact complaint the rest of this post is about.
Timeouts, Sampling, and Remote Ollama
Requests are bounded.ollamaTimeout
(default 300 seconds) caps how long a generation request can take, and when it fires you get a message naming the elapsed timeout and the setting to raise. There's also a live spinner with an elapsed-seconds counter during generation, auto-suppressed when stdout isn't a TTY, so a model loading into VRAM looks different from a stalled one. Cold-loading a large model can take a while on the first request, so raise the timeout rather than lowering it.
The corpus gets described, not sampled. This one changed, and if you have notes on the old behavior, throw them out.
The old version pasted an evenly-spaced sample of raw plaintexts into the prompt, capped at ollamaMaxSampleLines
. Two things were wrong with that. One, a sample carries no frequency information, so the model cannot tell a baseword that accounts for eight percent of the organization from a one-off that appeared once. Two, 500 plaintexts run two to three thousand tokens, which did not fit the 2048-token context window it was being sent into, so Ollama was quietly truncating the prompt.
Now the whole file gets aggregated in a single pass into a bounded statistical summary: Baseword shares with counts, masks, casing, lengths, trailing digits and symbols, and years. Roughly 850 tokens for a 41,887-password corpus, in about two tenths of a second. The context window went to 8192
to match, and it's OLLAMA_NUM_CTX
in .env
now.
ollamaMaxSampleLines
still exists but its meaning inverted. It's no longer a cap on what gets sent. The statistics are always sent, and the number is now the threshold under which literal plaintexts get included as well. Small corpus, the model sees both. Large corpus, it sees statistics only.
Two things result by doing it this way. $HEX[...]
plaintexts get decoded before they're counted, and a corpus that is more than a quarter hash-shaped lines gets a warning, because pointing this at an uncracked NTDS dump is a mistake you make exactly once.
OLLAMA_HOST accepts a scheme.http://
is prepended only when no scheme is present, and trailing slashes are stripped since callers append paths. That means http://box:11434, https://ollama.example.com
, and the bare host:port
default all work, so you can reach a remote Ollama over TLS.
LLM Config Reference
Every one of these lives in .env now, not config.json. That's a breaking change and Part 3 covers why.
| Default | Purpose |
|
| Which backend to talk to: |
|
| Bearer token; the default is the literal string |
|
| Endpoint; bare |
|
| Model for candidate generation; needs good JSON adherence |
|
| Context window; was |
|
| Seconds before giving up on a generation request |
|
| Corpus size under which literal plaintexts are sent alongside the statistics |
|
| Pre-fill industry and location from local model research in Target mode |
|
| Refuse |
That last one is new and you should probably turn it on. Ollama will happily proxy a cloud-hosted model to ollama.com through the same localhost
endpoint a local model uses, and these prompts carry recovered plaintexts and your client's name. OLLAMA_NO_CLOUD=1
refuses it before a request is assembled, and it now checks the destination address rather than just the model tag, which matters a lot more once the backend itself can be pointed anywhere. The long version is in Part 3.
Speaking of which, the two LLM_*
keys at the top of that table are what let you point this at something other than Ollama. vLLM and any OpenAI-compatible server work now. The bug that forced the issue is a good one. The API key used to be hardcoded, so a vLLM server started with --api-key
handed back a 401 and there was no key to set. OLLAMA_*
still owns host, model, timeout, context, and sampling for every backend, which is a slightly odd naming outcome I decided not to churn everyone's .env
over.
PassGPT is gone, and the reason in the opener is the one that actually decided it. Fine-tuning a model on recovered client passwords put that plaintext into weights on disk by way of an ML stack I couldn't vouch for, and I couldn't make the data handling defensible. The heavy dependencies and the training time were real problems too, and the Ollama path reaches useful candidates faster without either, but those were arguments. The DLP question was the answer.
If you were using it, mode 3 above is the closest replacement, and it's the same idea done locally. Your cracks go to an Ollama server you control instead of into a fine-tune.
Wordlist Tools (80)
Seven preprocessing utilities backed by hashcat-utils binaries, plus the wordlist optimizer at 8 and the two wordlist downloaders that moved here in 2.16.0 (Hashmob at 9
, Weakpass at 10
). Every file and directory prompt supports tab completion. Binaries live in hate_crack/hashcat-utils/bin/
.
Key | Tool | Binary | What it does |
1 | Filter by Length |
| Keep only words between a min and max length |
2 | Require Char Classes |
| Keep only words containing all required classes |
3 | Exclude Char Classes |
| Drop words containing any excluded class |
4 | Extract Substring |
| Cut a byte range from each word |
5 | Split by Length |
| Per-length output files in a directory |
6 | Subtract Wordlist |
| Remove words appearing in other files |
7 | Shard Wordlist |
| Split into N interleaved parts |
Character class mask bits for options 2 and 3 are additive: 1
lowercase, 2
uppercase, 4
digit, 8
symbol, 16
other. So 7
means lowercase plus uppercase plus digit.
Option 6 has two modes: mode 1 uses rli2.bin
against a single remove file, mode 2 uses rli.bin against multiple. This is how you avoid re-cracking. Subtract everything you've already recovered from a candidate list before you spend GPU time on it.
Option 1 pairs with your hash mode's known constraints. If the target enforces an 8-character minimum, filtering the wordlist beats letting hashcat burn through candidates that can't be right.
Sharding Changed Shape in 2.11.0
Update your notes on this one, because the prompts are different from older versions: it takes an input wordlist, an output base path, and a shard count, instead of a modulus and an offset.
It writes all N parts in a single pass, named with zero-padded part numbers: base.001, base.002
, through base.00N
. One run, every shard.
Each part is interleaved, every Nth line, not a contiguous chunk. That's deliberate. Every shard becomes a representative sample of the whole list, so no node gets stuck grinding only the low-probability tail while another finishes early. Copy one part per machine and point each node's hashcat at its own part.
On a single GPU, sharding buys you nothing in total throughput, but one part is still a fast, representative triage pass before you commit to the full list.
Wordlist Optimizer
wordlist_optimizer.py
is reachable from the menu now, instead of being a standalone script you have to remember. It accepts comma-separated inputs, and each entry can be a file or a directory, which gets expanded to the wordlist files inside it. Entries that don't resolve are reported rather than silently skipped.
Rule File Tools (81)
Preprocesses hashcat rule files. The first three operations read an input file and write to a separate output file, so the original is never modified.
- Clean (1): Strips invalid syntax and duplicate rules via
cleanup-rules.bin
. Run this after concatenating rule files or pulling rules from anywhere you don't control. - Optimize (2): Consolidates redundant operations via
rules_optimize.bin
, reducing file size and improving throughput. - Clean and optimize (3): Both in sequence through a temp file, then writes the final result.
- Download rules from Hashmob.net (4): the old main-menu
90
, moved here in 2.16.0. - Analyze Hashcat rules (5): Opcode statistics, the old main-menu
91
. This is the HashcatRosetta integration, and it's what Part 2 is about.
cleanup-rules.bin
requires a mode
argument (1
= CPU, 2
= GPU) and prints usage text without one. hate_crack passes it for you, defaulting to GPU, so Clean works without any setup on your end. Make sure you're on 2.11.3 or later for it.
Notifications (82)
Pushover push notifications when attacks finish and, optionally, per individual crack. All controls are under option 82:
- Toggle Pushover Notifications: Master switch, persists as
notify_enabled
- Toggle Per-Crack Notifications: A background tailer watches the
.out
file and pushes per crack with per-tick burst aggregation, persists asnotify_per_crack_enabled
. This can't be enabled while the master switch is off. - Send Test Pushover Notification: Fires a canned push, and works even with the master switch off so you can validate credentials before relying on them
Credentials and tuning stay file-only, and the two credentials moved to .env
in the config split:
Key | File | Default | Purpose |
|
| "" | Required for any push to fire |
|
| "" | Required for any push to fire |
|
| [] | Attacks that auto-consent without the |
|
| Silences nested attacks inside Quick/Extensive/Brute-Force wrappers; the wrapper sends one summary instead |
|
|
| Per-crack tailer burst cap |
|
|
| Per-crack tailer poll interval |
|
Leave notify_suppress_in_orchestrators
on. The Extensive methodology runs a long chain of sub-attacks, and without suppression you get a notification per link instead of one summary.
Consent is tracked under the same attack name you were prompted with, and that name flows down to both the job-done summary and the per-crack tailer. So, answering always
for Quick Crack actually allowlists Quick Crack, and the notifications you asked for are the notifications you get, including for Loopback, Combinator, PRINCE-LING, and N-gram (#110, fixed in 2.10.4).
Hashview Integration (94)
I verified the client against Hashview v0.8.3-dev
and built it to handle the fact that the API surface differs across server versions.
Routes and Version Compatibility
Customer → hashfile listing prefers GET /v1/customers//hashfiles
, one request covering every hash type. Where that route doesn't exist, it falls back to the type-scoped GET /v1/hashfiles/hash_type/
and asks which single type to enumerate, rather than sweeping them all on your behalf.
That listing route only exists on Hashview builds from June 08, 2026 onward (the v0.8.3-dev branch). On main or older servers, there is no hashfile-listing API at all, so the flow degrades gracefully. Enter the hashfile ID directly (look it up in the Hashview web UI) and its type resolves via GET /v1/getHashType/.
The rest of the route map: hash-type lookup is GET /v1/getHashType/
, uncracked "left" hash download is GET /v1/hashfiles/, delete_job
is DELETE /v1/jobs/
, and start_job
is a POST
. There are two things Hashview simply doesn't expose: First, there's no stop-job route, so stop_job
raises with guidance to use delete_job
instead of appearing to work. Second, there's no bulk cracked-hash export, so the best-effort "found" merge degrades gracefully.
Upload Correctness
Here are two things the uploader handles so the server doesn't reject your work.
$HEX[...] plaintexts. Hashcat emits $HEX[...]
for any recovered password containing leading or trailing whitespace or non-UTF-8 bytes. A Hashview that verifies plaintext against the hash looks at that literal string, fails to re-hash it, and throws out the entire batch. One oddball password takes thousands of good ones with it, and the server-side error tells you nothing useful.
The uploader now decodes them to the exact bytes the server needs: latin-1 to UTF-8 for the UTF-16LE modes (NTLM 1000, MSSQL 1731), raw bytes for the raw-byte modes (0/100/300/900/1400/1700). When inlining would be unsafe, meaning an embedded CR or LF, it keeps the $HEX
wrapper verbatim so a $HEX
-aware server can still handle it. Verified end-to-end against an unpatched Hashview. One oddball password in a batch of thousands no longer costs you the batch.
Client-side validation. Every hash:plaintext pair is checked against the declared hashcat mode before upload, which is a length check for wrong-width hashes, plus a full plaintext recompute for the reproducible fast modes (MD5, SHA1, MD4, NTLM, and SHA2-256/512). A stray MD5 mixed into an NTLM list is skipped with a per-line warning instead of failing the batch server-side, and it raises clearly if nothing valid remains. It also bundles a pure-Python MD4, which it shouldn't have to, but here we are. OpenSSL 3 dropped MD4, so hashlib.new("md4")
just raises on any modern distro, and you cannot verify a single NTLM hash without it. Opt out of the whole thing with validate=False
.
Upload Reporting
Uploads report what landed. You get the number of pairs the client sent and how many it skipped in validation, available regardless of server version, and against a Hashview that reports import counts, also how many were newly cracked, verified, and left unmatched (already cracked or not present). upload_cracked_hashes
surfaces uploaded
, skipped
, and skipped_cached
in its return value alongside any server-provided counts. A server that rejects a plaintext no longer costs you the batch either. The offending lines are named, up to ten of them, and the rest uploads without them.
Other Hashview additions
• Rule file download.list_rules()
and download_rules()
wrap GET /v1/rules
and GET /v1/rules/{id}
. The server gzip-compresses plaintext rules on the fly, so downloads are decompressed before saving and the result works directly with hashcat -r
. Available in the menu as "Download Rule" and on the CLI: hate_crack --hashview download-rules --rules-id [--output ]
. There's a "Download All Rules" option alongside it now, backed by download_all_rules(), which walks every rule the API knows about and reports per-rule success and failure counts instead of abandoning the batch on the first error.
• Env var overrides.HASHVIEW_URL
and HASHVIEW_API_KEY
are .env
keys now, and a real exported environment variable still overrides the file for a single run, so you can point at a dev stack without editing persisted config.
• Tolerant response parsing./v1/customers
returns its users
array as native JSON on current servers and as a double-encoded string on older ones (Hashview issue #229); both shapes are accepted. get_hashfile_hash_type
reads the hashfiles
envelope array the endpoint actually sends. And hash type is read by key presence, not truthiness, which is what makes mode 0 (MD5) resolve correctly instead of being treated as absent.
• Local integration-test harness.HASHVIEW_TEST_LOCAL=1
with HASHVIEW_REPO=
spins up a Hashview docker stack, seeds an admin API key, runs the live tests against it, and tears it down (HASHVIEW_KEEP=1
keeps it). This harness is what surfaced and verified every route fix above.
Three Things that Made it Usable on a Real Hash List
Customer hashfile listing is one request now. There's a GET /v1/customers//hashfiles
route on Hashview v0.8.3-dev
and later, which replaces the old 26-request sweep across common hashcat modes and covers every hash type instead of the ones I thought to sweep.
Older servers 404 that route, and what happened next was worse than slow. The client swept all 26 types unconditionally, and that route's cost scales with the number of hashes of a type rather than the number of files, because the server counts total and cracked per hashfile. When measured against a production instance, 73 small NetNTLMv2 captures listed in 0.6 seconds, while 54 NTDS dumps holding 7.85M hashes took 549 seconds to produce 13 KB of JSON. The 30-second client timeout always expired on NTLM, and the handler read, e.response.status_code
on a timeout, which is None
, which wasn't the 404 break and fell through to continue
. The resulting net effect is about 39 seconds of waiting, then a hashfile list missing every NTLM file, presented exactly like a customer who genuinely has none. NTLM is where domain dumps land, so the answer was usually empty and never marked as incomplete.
A timeout is caught separately from an HTTP error now, reported on stdout instead of debug-gated, and the sweep gives up after two timed-out types instead of spending the budget 26 times. Where the one-request route is missing, you get asked which type to enumerate, defaulting to 1000, with A for the full sweep (warned as slow) and S to skip straight to typing the ID. The listing timeout is its own constant and deliberately wasn't raised, because no timeout worth having rescues a nine-minute query. Also, a hashfile ID you type by hand is no longer rejected for being absent from a partial listing, which is what used to happen the moment a one-type sweep left a non-empty list holding none of the customer's other files. When measured on the same instance, skip answers in 0.4 seconds, a fast type returns six real hashfiles in 1.0, and NTLM now fails in 30.4 naming the type and telling you to read the ID off the web UI.
The underlying cost is a server-side N+1 and isn't fixed here. Both listing routes, and the web UI's own hashfiles page, run two COUNT queries per hashfile inside a Python loop where one GROUP BY
would do.
Uploads are batched.upload_cracked_hashes
POSTs in 10,000-line chunks to /v1/hashes/import/
, and a re-run resumes rather than starting over.
Uploads de-duplicate. There's a cache at ~/.hate_crack/hashview_uploaded_cache.txt
, namespaced per operation, so re-running an upload doesn't re-push everything you already sent. Which you will do, because the natural response to an ambiguous upload result is to run it again.
Non-Interactive Mode
You can now launch a single attack without the menu, which makes hate_crack scriptable for the first time (Issue #17).
# Quick crack: one wordlist plus optional rule(s) from the rules directory
hate_crack quick hashes.txt 1000 --wordlist rockyou.txt --rules best64.rule
# Chain two rules within a single hashcat invocation
hate_crack quick hashes.txt 1000 --wordlist rockyou.txt --rules best64.rule+d3ad0ne.rule
# Run two rules as two separate passes
hate_crack quick hashes.txt 1000 --wordlist rockyou.txt --rules best64.rule d3ad0ne.rule
# Configured-wordlist dictionary methodology
hate_crack dict hashes.txt 1000
# Brute force lengths 1-8
hate_crack brute hashes.txt 1000 --min 1 --max 8
# Top mask targeting roughly 4 hours
hate_crack topmask hashes.txt 1000 --target-time 4
The four attack subcommands are quick
, dict
, brute
(--min
/--max
), and topmask
(--target-time
). There are two more trees that aren't attacks: hate_crack hashview ...
and hate_crack coverage status|history|forget --hashfile X
.
Mind the --rules
distinction, because it's easy to get wrong: a.rule+b.rule
with a plus chains both rule files inside one hashcat run, while two space-separated names run as two sequential passes. Those are very different amounts of work.
Pre-processing prompts that would normally block (computer-account filtering, LM-first brute force, and duplicate-account dedup) auto-accept their defaults in this mode. Exit codes are real: 0 on success, non-zero for a missing hash file, a non-numeric hash type, a missing wordlist, or an unknown rule filename. That's what makes it safe to put in a loop or a pipeline instead of wrapping it in expect
.
Pass --exit-code-on-skip
and a run that Attack Coverage skipped as already-covered exits 3
, which is the difference between a loop that logs "nothing to do" and one that logs "it worked" about an attack that never ran.
Rebuilding Your Results File (93)
This is a small option, but the on day you need it, you'll be glad it's there.
.out
is where hate_crack keeps your cracks, and everything downstream reads it: pipal, the Excel export, Loopback, the LLM cracked-password modes, and the Spoonman corpus. hashcat's potfile holds the same plaintexts independently. When the .out
file gets truncated, clobbered, or deleted, nothing is actually lost yet, and option 93 rebuilds it from the potfile.
It tells you how many cracked hashes the current file has before overwriting it and asks for confirmation, auto-confirming when stdin isn't a terminal. --restore-potfile
does it at startup and then drops you into the normal menu, skipping the prompt because you asked explicitly. It's a persistent setting (restore_potfile_on_start
) with --no-restore-potfile
to override.
I tested this against a 22MB NTLM run truncated to zero bytes and got 2,461 cracked hashes back.
Things it Now Figures Out for You
username:hash auto-detection. A dedicated module (hate_crack/username_detect.py
) owns an allowlist of hash modes where a username: prefix is common, along with the expected hex length of the hash field for each: MD5 and variants at 32, SHA1 family at 40, MD4 and NTLM at 32, SHA2-256 family at 64, SHA2-512 family at 128, and so on. It validates per line and passes --username
to hashcat when warranted, so you stop remembering the flag and stop getting confusing failures when you forget it.
Computer account filtering. For NetNTLM modes (5500/5600) and NTDS input generally, hate_crack counts usernames ending in $, tells you how many it found, and offers to filter them out (defaulting to yes). Machine account passwords are 120 random characters, and you are not cracking them. This is because leaving them in only inflates your hash count and deflates your crack percentage. NetNTLM dedup landed alongside it.
Bare NTLM detection is tolerant of Windows exports. Leading blank lines, BOM characters, and NULL bytes from UTF-16 encoding are all handled, which matters because that's exactly what you get out of Windows tooling. When a format genuinely isn't recognized, the error shows the actual first-line content and lists the expected formats, so you can see what it choked on.
Optimized kernel (-O) per attack. This is controlled by optimizedKernelAttacks
in config, which is an explicit list of internal attack function names: hcatDictionary
, hcatQuickDictionary
, hcatBandrel
, hcatBruteForce
, hcatTopMask
, hcatAdHocMask
, hcatMarkovBruteForce
, hcatFingerprint
, the combinator family, hcatPrince
, hcatPermute
, hcatPCFG
, hcatRosettaMask
, hcatCorporateMasks
, hcatSmartMask
, and others. Remove an entry to stop passing -O
for that attack. This matters because -O
caps password length (typically 32 for fast hashes), so it's a throughput win that can silently exclude long candidates, which is why it's a per-attack list instead of a global flag.
The list being a whole-list opt-in has a nasty consequence, and it took two incidents to make me add a warning for it. A config.json
written before an attack existed pins the list without that attack, so the attack never gets the optimized kernel, forever, and the only symptom is that it's slower than it should be. hcatRosettaMask
shipped degraded for eleven days before anyone noticed and hcatCorporateMasks
was degraded the day it landed. Startup warns about missing entries now, naming each one and the file it came from. It warns rather than repairing the list, because "written before this existed" and "deliberately opted out" are the same bytes on disk and I can't tell them apart.
Sharp Edges, Filed Down
Tab completion on path prompts. This started in Wordlist Tools and spread. As of 2.14.1 the p. Enter a custom path
branches in the OMEN and Markov training pickers, the combipow wordlist prompt, and the rule cleanup/optimize output prompts all route through select_file_with_autocomplete
instead of a bare input()
where TAB did nothing. The same release fixed a completer leak, so a numeric menu right after a file picker no longer tries to autocomplete file paths at you.
Arrow-key menus.HATE_CRACK_ARROW_MENU=1
enables arrow navigation, and it works throughout, including the LLM and OMEN submenus, which route through the shared interactive_menu
helper like everything else.
Typos don't abort attacks. Mistyping a wordlist number or a generation mode re-prompts instead of dropping you back to the main menu, and every picker offers an explicit cancel. Rejected menu keys are reported, not swallowed, so you will know the keystroke landed.
Double Ctrl+C within two seconds. This returns you to the main menu instead of killing the process. Single Ctrl+C still interrupts the running attack.
Fingerprint Attack skips empty input. When there are no candidates to expand, it says so and skips, including the secondary hybrid pass, rather than launching hashcat sessions against an empty wordlist.
LC_ALL=C on every sort -u subprocess covers the fingerprint expander pipeline, _write_field_sorted_unique
, and LM-to-NT combinator dedupe. Cracked passwords containing non-UTF-8 bytes will otherwise produce sort: Illegal byte sequence
on macOS, which takes the fingerprint candidate list with it. Forcing the C locale keeps byte-oriented sorting predictable regardless of platform.
Pipal handling.$HEX[...]
rows keep their trailing newline through unhexlify().decode()
, so each cracked password lands on its own line in the .passwords
file pipal consumes. Otherwise, HEX rows concatenate with their neighbors and skew the counts. Baseword parsing reads the Top N base words
section line by line, returning up to pipal_count
words and stopping at the end of the section. Therefore, a small crack with fewer unique base words than pipal_count
(default 10) still reports what it found. The subprocess is spawned with list-form arguments instead of a formatted shell=True
string, so hash file paths containing shell metacharacters stay inert.
Download and wordlist handling. Weakpass downloads run through a proper transmission daemon that watches /tmp/hate_crack/
for new .torrent
files while content lands in your configured wordlist directory, with daemon stdout suppressed and a 30-second watch-dir polling window to accommodate transmission's ~10-second scan interval. Already present wordlists are skipped instead of re-downloaded. Hashmob rule downloads are parallelized with a thread pool (up to four concurrent), report a success/failure summary, and are paced by a real rate limiter. .7z
, .torrent
, and .out
files are filtered out of wordlist selection menus, and external binaries auto-detect gzip input. lineCount
uses binary chunk counting, which matters on multi-gigabyte lists. The d3ad0ne and T0XlC rule passes run in a single hashcat invocation.
Can You Trust the Tags?
If you contribute, or you just want to know whether a release is worth pulling:
CI runs ruff
, ty
, and pytest
on Python 3.13 for every pull request and every push to main
, plus bandit
against a baseline so only new findings fail, and pip-audit
for known CVEs. Tagging is gated on it passing, so a release can only be cut from a commit that CI validated, regardless of whether the author had local hooks installed.
2.15.0 added an end-to-end suite for the four non-interactive subcommands, which is the part of the tool most likely to be running unattended and least likely to have anyone watching when it breaks. It's opt-in behind HATE_CRACK_RUN_E2E=1
, since it wants real binaries and real fixtures. One detail from building it that will eventually bite you somewhere else is this: the NTLM fixture generator ships a pure-Python MD4, because hashlib.new("md4")
raises on any OpenSSL build with legacy providers disabled, which is most of them now. That's the second place in this codebase that had to carry its own MD4 to work with NTLM at all, the other being the Hashview uploader's validation path.
2.15.0 also dropped beautifulsoup4
as a runtime dependency. The Weakpass listing parser was its only consumer, and weakpass.com is an Inertia app, so the data was already sitting there as JSON in a data-page
attribute the whole time. Reading that with the standard library is both less code and less fragile than parsing the rendered page, and it's one fewer dependency in a tool people install on engagement boxes.
Two tracks, briefly, because Part 3 tells this story properly and I'd rather not tell it twice. Development lands on nightly-dev
and gets tagged there as a release candidate for whatever version the batch is heading toward: v2.20.1rc1, v2.20.1rc2
. Merging down to main promotes that same target to its final. A plain git pull
on main
gets you releases, and --nightly
or the branch gets you what's landed since.
If you're reading tags to decide what to run, stay on releases unless you want a specific thing that hasn't shipped yet. The whole point of tagging nightlies is that everything CI validated is addressable, not that you should be living on it.
Tagging is serialized, so two merges landing back to back can't collide, and a re-run against an already-tagged commit is a clean no-op. The workflow creates the GitHub release itself instead of relying on the tag push to trigger a second workflow, because GitHub suppresses workflow triggers for events created with the default GITHUB_TOKEN
. All actions are pinned to real releases with accurate version comments.
Dependabot watches Python (uv
) and GitHub Actions deps weekly and points at nightly-dev
rather than main
. 2.14.3 added a detect-private-key
pre-commit gate, because bandit only covers hate_crack/
, so nothing was inspecting config files, docs, or test fixtures for committed key material. Which turned out to be the least of what nothing was inspecting, and that's the opening story of Part 3.
Local uv
Python is pinned to 3.13 via .python-version
, because a fresh worktree would otherwise refuse to build at all. requires-python = ">=3.13"
is an honest constraint, and uv
honored it by grabbing CPython 3.15.0a7. That's a pre-release, and it satisfies >=3.13
on the version comparison alone, but pyo3 0.26 does not build against it, so jiter
, fastuuid
, and pydantic-core
all go down together. Pin your interpreter.
New Config Keys Since 2.0
Everything added along the way is now in one place. These all live in config.json
. Anything that used to be here and isn't now moved to .env, and that list is in Part 3.
Key | Default | Area |
|
| Potfile location; |
|
| Where rule-attack debug logs land; the Rosetta Attack reads these |
|
| Pre-optimized wordlist dir; Quick Crack default |
|
| Optional dictionary for the Fingerprint Attack's combination pass |
|
| Cap on corpus profiling for the LLM statistics pass |
|
| Hybrid Attack time budget in seconds; |
|
| Passwords that must share a skeleton before Smart Mask calls it a pattern |
|
| OMEN candidate cap |
|
| PCFG / PRINCE-LING grammar name; was |
|
| PCFG candidate cap; I raise this too |
|
| PRINCE-LING base wordlist cap |
|
| Which attacks pass |
|
| Startup version check |
|
|
|
|
| Rebuild |
|
| Whether rule attacks write debug logs at all |
|
| Whether Attack Coverage records and offers to skip |
|
| Debug logging; writes cracked plaintexts to disk, so mind it |
|
| Minimum Weakpass rank to consider; |
|
| Pushover master switch |
|
| Per-crack notifications |
|
| Auto-consent attack names |
|
| Silence nested attacks |
|
| Tailer burst cap |
|
| Tailer poll interval |
hcatOptimizedWordlists
deserves a note: it's the directory for pre-optimized wordlists, it falls back to hcatWordlists
if not found, and it's the Quick Crack default. The numbered list and tab completion browse hcatWordlists
, but pressing Enter still falls back to hcatOptimizedWordlists
, which was a deliberate fix in 2.10.9 rather than an inconsistency.
How it works
Once you click Generate, Ollama reads this article and crafts 5 comprehension questions. Your answers are graded against the article content — general knowledge won't be enough. Score 70+ to count toward your certificate.
Questions are cached — you'll always get the same 5 for this article.