* fix: use sys.executable for PCFG/PRINCE-LING subprocesses
pcfg_guesser.py and prince_ling.py were launched via a bare "python3"
resolved from PATH, so they ran under whatever interpreter happened to
be first on PATH instead of the pinned 3.13 hate_crack itself runs
under. sys.executable always resolves to that interpreter, which also
makes the shutil.which("python3") presence check unnecessary.
Fixes#149
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: match pcfgRuleset default casing to on-disk Rules/Default
pcfgRuleset defaulted to "DEFAULT" but pcfg_cracker ships Rules/Default
(capitalized, not all-caps). Case-insensitive filesystems (macOS)
masked the mismatch; on case-sensitive filesystems (most Linux) both
PCFG-based attacks failed out of the box.
Fixes#148
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: keep PCFG/PRINCE-LING child stdin open to avoid silent truncation
hcatPCFG launched pcfg_guesser.py with no stdin=, so it inherited
hate_crack's own stdin. Whenever that stdin isn't a TTY (cron, CI,
detached runs, piped input), pcfg_guesser's keypress-listener thread
hit EOFError on its first input() call and the guesser shut itself
down after tens of thousands of candidates, silently ignoring
--limit/pcfgMaxCandidates. Passing stdin=subprocess.PIPE keeps the fd
open without writing to it, so input() blocks instead of raising.
prince_ling.py comes from the same upstream project and shares the
keypress thread, so hcatPrinceLing gets the same treatment.
Fixes#146
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* style: apply ruff format to hcatPCFG Popen call
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: catch OSError loading config.json.example defaults
The config.json.example defaults load only caught JSONDecodeError, so
a missing or unreadable file (e.g. a dangling symlink surviving a
git-archive tarball, docker COPY, or partial /opt/hate_crack install)
escaped as an uncaught FileNotFoundError instead of the "package
installation issue" message already written for exactly this case.
Extracted the load into _load_config_defaults() so the failure paths
are unit-testable, and added a dangling-symlink-specific message.
Fixes#155
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* style: apply ruff format to _load_config_defaults
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: replace brittle config.json.example test guards
test_packaged_example_is_symlink_to_root asserted symlink-ness, an
implementation detail that setuptools dereferences in every built
wheel/sdist — the test only ever passed in the source tree.
test_packaged_and_root_examples_have_identical_content compared the
same inode to itself there, making it a tautology. Neither would catch
real drift in a distributed artifact. Replaced both with an explicit
expected-key-set assertion (the substantive invariant #150 needed) and
a content-parity check that's only exercised outside the source tree.
Fixes#154
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: share config.json.example defaults between api.py and main.py
api.py's get_hcat_wordlists_dir/get_rules_dir/get_hcat_tuning_args/
get_hcat_potfile_path read config.json directly and fell back to their
own hardcoded, cwd-relative defaults for any key absent from disk.
main.py merges config.json.example into config_parser in memory (since
af46f59 stopped backfilling it to disk), so any key missing from a
user's config.json now resolves differently depending on which module
resolved it — e.g. rules_directory: <hate_path>/hashcat/rules in
main.py vs <cwd>/rules in api.py, silently creating an empty directory
under whatever directory the user launched from.
Added _load_config_defaults()/_load_merged_config() in api.py so both
modules perform the same config.json.example-then-config.json merge.
Fixes#153
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: revert inert hcatPrinceLing stdin=PIPE change
prince_ling.py never imports lib_guesser.cracking_session (the only
file with input()/a keypress thread) and never reads stdin, so there
was nothing for stdin=PIPE to fix there. Worse, subprocess.run(...,
stdin=subprocess.PIPE) with no input= argument closes the pipe
immediately via communicate(None), so the change was inert regardless.
The hcatPCFG side of #146 (Popen with stdin=PIPE, which correctly
keeps the write end open) is unaffected and remains the actual fix.
* fix: resolve pcfgRuleset case-insensitively against Rules/ on disk
Users with a config.json predating this stack's default-casing fix
have "DEFAULT" backfilled to disk (hate_crack used to write missing
keys to disk; see af46f59). For them, the new "Default" default in
config.json.example is never consulted, so Rules/DEFAULT still
doesn't resolve on case-sensitive filesystems and both PCFG attacks
stay broken. Resolve the configured ruleset name case-insensitively
against whatever casing actually exists under pcfg_cracker/Rules/, so
both a stale "DEFAULT" on disk and a user typo like "default" resolve
to the real Rules/Default directory.
* fix: resolve relative hcatPotfilePath against hate_path, not config dir
get_hcat_potfile_path resolved a relative hcatPotfilePath against the
config.json's own directory, while main.py resolves it against
hate_path unconditionally. These only agreed when config.json lived
at hate_path — they diverged for the ~/.hate_crack candidate root,
which is a first-class supported config location. This was the exact
divergence class #153 set out to eliminate, left inside the function
meant to fix it.
* fix: use resolved ruleset basename for prince_ling argv and cache path
hcatPrinceLing's ruleset existence check was fixed to resolve
pcfgRuleset case-insensitively against Rules/ on disk, but the
subprocess argv passed to prince_ling.py and the cache filename both
still used the raw, unresolved value. prince_ling.py does its own
(case-sensitive) Rules/ lookup internally, so a legacy "DEFAULT"
config would pass hate_crack's precheck and then fail inside
prince_ling.py itself with a confusing "generation failed" message.
Use the resolved basename in both places, matching what hcatPCFG
already does for its --rule argument.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
The undocumented in-code default (1,000,000) let OMEN finish in ~2s
when config.json omitted the key, far below the 50,000,000 shipped in
config.json.example. Align both to 100,000,000 per issue #145.
Target-info mode (menu 12) asked for company, industry, and location as three
blank prompts. Once the company name is known the local Ollama model can often
supply the other two, so ask it and offer the answers as editable defaults.
- llm.research_target(): TargetResearchInput/TargetResearchOutput schemas plus a
research SystemPromptGenerator that tells the model to return empty strings
when it does not genuinely recognize the organization, so an unknown small
client yields blank prompts instead of a confident hallucination.
APITimeoutError is translated to LLMTimeoutError like generate_candidates.
- clean_research_field(): strips, collapses whitespace, caps at 80 chars, and
turns anything non-string into "" so model output cannot be pasted unbounded
into a prompt default.
- main.hcatOllamaResearchTarget(): runs the call inside the existing spinner and
degrades to blank suggestions on timeout or any other failure, so research can
never block the attack.
- attacks.ollama_attack(): shows suggestions as "Industry (freight rail): "
defaults, labelled explicitly as the model's GUESSES rather than OSINT.
- New ollamaAutoResearch config toggle (default true) in both config examples.
Research uses only the configured local Ollama server; the client name is never
sent to a search engine or third-party company-data API.
Co-Authored-By: Claude <noreply@anthropic.com>
Defect 1 — hcatOllama showed a plain print then blocked silently for
up to 300 s while waiting for the LLM. A new generic context manager
`hate_crack/progress.py::spinner()` now runs a daemon thread that
repaints a single line with a frame + elapsed-seconds counter every
~120 ms. TTY guard ensures non-TTY/test environments get a single
plain print instead of ANSI control characters. The line is erased
with \033[2K\r on exit (normal and exceptional).
Defect 2 — the wordlist path materialised every line in memory and
built a prompt that could massively overflow ollamaNumCtx on large
wordlists. The path now does a two-pass evenly-spaced sample: pass 1
counts usable lines, pass 2 stride-selects up to `ollamaMaxSampleLines`
(default 500, new config key) spread across the full file. When no
capping occurs the message reads "Loaded N passwords"; when capping
occurs it reads "Sampled N of M passwords".
Co-Authored-By: Claude <noreply@anthropic.com>
The Atomic Agents client was built with no timeout, so an Ollama server that
accepted the TCP connection but never replied (most commonly a large model still
loading into VRAM) left the CLI blocked in agent.run() forever. The caller's
`except Exception` handler only ever fired on connection-refused.
- llm.generate_candidates() takes a `timeout` parameter (default
DEFAULT_TIMEOUT_SECONDS = 300.0) and forwards it to the OpenAI client.
- openai.APITimeoutError is translated into a new domain-level
llm.LLMTimeoutError so main.py need not import openai itself, keeping the
atomic-agents/instructor dependency isolated to llm.py as documented.
- hcatOllama passes the new `ollamaTimeout` config value and prints
timeout-specific guidance (elapsed seconds, VRAM-loading hint, the setting to
raise) instead of the misleading "ensure Ollama is running" message.
- Documented `ollamaTimeout` in config.json.example and README.
Also closes two test gaps: the defensive `except ValueError` handler in
hcatOllama now has coverage, and the misleadingly-named `test_unknown_mode_raises`
is split into explicit unit tests of _build_request's mode validation. Ticked the
completed steps in the LLM Atomic Agents plan doc.
Co-Authored-By: Claude <noreply@anthropic.com>
Adds notify_* keys to both config.json.example files, threads
notification calls through hashcat invocations in main.py, and
exposes menu/attack hooks. Pushed for manual testing — verification
and PR still pending.
Refs #106
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-add hcatOptimizedWordlists config key (previously removed) with a
default of ./optimized_wordlists. Falls back to hcatWordlists if the
configured directory does not exist.
Update quick_crack to list files from and default to hcatOptimizedWordlists
instead of hcatWordlists when prompting for a wordlist or directory.
- Apply os.path.expanduser() to hcatPath so tilde (~) paths work,
consistent with every other config path (hcatWordlists, rulesDirectory,
hcatPotfilePath, hcatDebugLogPath)
- Improve error message when binary not found to show what hcatPath was
checked, making it easier to diagnose misconfiguration
- Update optimizedKernelAttacks in config.json.example to include all 21
attacks from DEFAULT_OPTIMIZED_ATTACKS (13 were added in v2.4.0 but
config.json.example was not updated, causing the runtime override to
exclude them)
- Merge combinator, combinator3, and combinatorX into one unified
combinator_crack function that routes by wordlist count:
2 (no sep) -> hcatCombination, 3 (no sep) -> hcatCombinator3,
4+ or any separator -> hcatCombinatorX
- Replace comma-separated wordlist input with one-at-a-time
tab-autocomplete prompts (blank line to finish)
- Add _prompt_wordlist_paths helper using existing readline infrastructure
- Add hcatCombinator3Wordlist and hcatCombinatorXWordlist config vars
with rockyou.txt defaults
- Print full hashcat command to stdout in --debug mode by calling
_debug_cmd at the end of _append_potfile_arg (covers all 27 invocations)
- Collapse combinator submenu from 6 options to 4; keep combinator3_crack,
combinatorX_crack, and combinator_3plus_crack as delegation shims
- Update tests to cover unified routing and new prompt interface
Extract _select_rules() helper from quick_crack/loopback_attack and wire
it into omen_attack so OMEN can run with rule chains. Extend hcatOmen()
to accept and apply an hcatChains argument including debug mode injection.
Fix resolve_path() to honour HATE_CRACK_ORIG_CWD (set by the install shim)
so relative hash/wordlist paths resolve against the caller's working directory
instead of the repo root. Increase default omenMaxCandidates to 50M.
- Add hashcat as git submodule; compile with make, skip if already in PATH
- Convert princeprocessor from tracked files to git submodule
- Change .DEFAULT_GOAL to install so plain `make` does a full install
- Install uv, Xcode CLT (macOS), build-essential (Debian) if missing
- vendor-assets falls back to system hashcat if submodule not compiled
- Remove hcatOptimizedWordlists; all attacks now use hcatWordlists
- Default hcatWordlists to ./wordlists, rules_directory to ./hashcat/rules
- Default hcatTuning to empty string (no --force --remove)
- Backfill missing config.json keys from config.json.example at startup
- Wrap hcatBin/hcatTuning/hcatWordlists loading in try/except with defaults
- Fall back to vendored hashcat binary at hate_path/hashcat/hashcat
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add ability to fine-tune PassGPT models on custom password wordlists.
Models save locally to ~/.hate_crack/passgpt/ with no data uploaded to
HuggingFace (push_to_hub=False, HF_HUB_DISABLE_TELEMETRY=1). The
PassGPT menu now shows available models (default + local fine-tuned)
and a training option. Adds datasets to [ml] deps and passgptTrainingList
config key.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add PassGPT as attack mode 17, using a GPT-2 model trained on leaked
password datasets to generate candidate passwords. The generator pipes
candidates to hashcat via stdin, matching the existing OMEN pipe pattern.
- Add standalone generator module (python -m hate_crack.passgpt_generate)
- Add [ml] optional dependency group (torch, transformers)
- Add config keys: passgptModel, passgptMaxCandidates, passgptBatchSize
- Wire up menu entries in main.py, attacks.py, and hate_crack.py
- Auto-detect GPU (CUDA/MPS) with CPU fallback
- Add unit tests for pipe construction, handler, and ML deps check
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add OMEN (Ordered Markov ENumerator) as a probability-ordered password
candidate generator. Trains n-gram models on leaked passwords via
createNG, then pipes candidates from enumNG into hashcat.
Also fix a pre-existing bug where ensure_binary() used quit(1) instead
of sys.exit(1) - quit() closes stdin before raising SystemExit, which
caused "ValueError: I/O operation on closed file" when any optional
binary check failed and the program continued to use input().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove ollamaWordlist config key and all references. Wordlist mode now
requires the cracked hashes .out file to exist and extracts passwords
by splitting on the first colon. Detect Ollama refusal responses and
abort gracefully. Update tests accordingly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove 500-line wordlist cap and send the entire file to Ollama.
Add ollamaNumCtx config key (default 32768) to control the context
window size. Invert wordlist prompt to default-yes, remove unused
ollamaCandidateCount config.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename markov_attack → ollama_attack and hcatMarkov → hcatOllama across
menu, attacks, and tests. Remove candidate count prompts and cracked-output
default wordlist logic. Rename config keys (markov* → ollama*) and drop
ollamaUrl. Fix Dockerfile.test to use granular build steps.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a new attack mode that uses a local LLM via Ollama to generate
password candidates, converts them into hashcat .hcstat2 Markov
statistics via hcstat2gen, and runs a Markov-enhanced mask attack.
Two generation sub-modes:
- Wordlist-based: feeds sample from an existing wordlist to the LLM
as pattern context (config-selectable default with Y/N override)
- Target-based: prompts for company name, industry, and location
for contextual password generation
Pipeline: Ollama API -> candidate file -> hcstat2gen -> LZMA compress
-> hashcat -a 3 --markov-hcstat2
Config additions: ollamaUrl, ollamaModel, markovCandidateCount,
markovWordlist. No new pip dependencies (uses stdlib urllib/lzma).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When hcatBin is a relative name (e.g. "hashcat"), construct the full
path by joining hcatPath and hcatBin so the correct hashcat binary is
used instead of relying on PATH resolution.