fix: write output files to user's CWD, not install directory

The bash shim uses `uv run --directory <install_dir>` which changes the
process CWD to the install directory. _ensure_hashfile_in_cwd() and the
Hashview download path used os.getcwd() to determine the target directory
for output files (.out, .nt, etc.), causing them to land in the install
directory instead of where the user ran the command.

Add orig_cwd() helper that reads HATE_CRACK_ORIG_CWD (set by the shim)
and use it in _ensure_hashfile_in_cwd(), the Hashview download path, and
the potfile fallback path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Justin Bollinger
2026-04-08 11:43:51 -04:00
co-authored by Claude Opus 4.6
parent d0bfb015a6
commit fa07d37a84
6 changed files with 76 additions and 42 deletions
+6 -3
View File
@@ -11,6 +11,7 @@ from typing import Callable, Optional, Tuple
import requests # type: ignore[import-untyped]
from bs4 import BeautifulSoup
from hate_crack.cli import orig_cwd
from hate_crack.formatting import print_multicolumn_list
_TORRENT_CLEANUP_REGISTERED = False
@@ -841,8 +842,10 @@ class HashviewAPI:
resp.raise_for_status()
if output_file is None:
output_file = f"left_{customer_id}_{hashfile_id}.txt"
# Convert to absolute path to ensure file is preserved if CWD changes
output_file = os.path.abspath(output_file)
# Resolve relative paths against the user's original CWD, not the
# install directory that ``uv run --directory`` may have switched to.
if not os.path.isabs(output_file):
output_file = os.path.join(orig_cwd(), output_file)
output_abs = output_file
total = int(resp.headers.get("content-length", 0))
downloaded = 0
@@ -869,7 +872,7 @@ class HashviewAPI:
# Try to download found file and process with hashcat
combined_count = 0
combined_file = None
out_dir = os.path.dirname(output_abs) or os.getcwd()
out_dir = os.path.dirname(output_abs) or orig_cwd()
found_file = os.path.join(out_dir, f"found_{customer_id}_{hashfile_id}.txt")
try:
+11
View File
@@ -4,6 +4,17 @@ import sys
from typing import Optional
def orig_cwd() -> str:
"""Return the caller's original working directory.
When invoked via the bash shim (``uv run --directory``), the process CWD
is the install directory, not where the user ran the command.
``HATE_CRACK_ORIG_CWD`` preserves the real CWD so that output files
(e.g. ``.out``, ``.nt``) are created next to the hashfile the user specified.
"""
return os.environ.get("HATE_CRACK_ORIG_CWD") or os.getcwd()
def resolve_path(value: Optional[str]) -> Optional[str]:
"""Expand user and return an absolute path, or None.
+3 -5
View File
@@ -69,6 +69,7 @@ from hate_crack.api import ( # noqa: E402
extract_with_7z,
)
from hate_crack.cli import ( # noqa: E402
orig_cwd,
resolve_path,
setup_logging,
)
@@ -198,10 +199,7 @@ def _ensure_hashfile_in_cwd(hashfile_path):
"""Ensure hashfile path points to cwd to keep output files in execution dir."""
if not hashfile_path:
return hashfile_path
try:
cwd = os.getcwd()
except Exception:
return hashfile_path
cwd = orig_cwd()
if not os.path.isabs(hashfile_path):
return hashfile_path
if os.path.dirname(hashfile_path) == cwd:
@@ -358,7 +356,7 @@ if "hcatPotfilePath" not in config_parser:
if os.path.isfile(_default_pot) or os.path.isdir(os.path.dirname(_default_pot)):
hcatPotfilePath = _default_pot
else:
hcatPotfilePath = os.path.join(os.getcwd(), "hashcat.potfile")
hcatPotfilePath = os.path.join(orig_cwd(), "hashcat.potfile")
else:
_raw_pot = (config_parser.get("hcatPotfilePath") or "").strip()
if _raw_pot == "":
+14 -25
View File
@@ -774,14 +774,8 @@ class TestHashviewAPI:
content = f.read()
assert content == b"hash1\nhash2\n"
def test_hashfile_orig_path_preservation(self, tmp_path):
def test_hashfile_orig_path_preservation(self, tmp_path, monkeypatch):
"""Test that original hashfile path is preserved before _ensure_hashfile_in_cwd"""
import sys
import os
sys.path.insert(
0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
)
from hate_crack.main import _ensure_hashfile_in_cwd
# Create a test hashfile in a different directory
@@ -792,27 +786,22 @@ class TestHashviewAPI:
original_path = str(test_file)
# Save current directory
orig_cwd = os.getcwd()
try:
# Change to tmp_path
os.chdir(str(tmp_path))
# Set HATE_CRACK_ORIG_CWD so _ensure_hashfile_in_cwd targets tmp_path
monkeypatch.setenv("HATE_CRACK_ORIG_CWD", str(tmp_path))
# Call _ensure_hashfile_in_cwd
result_path = _ensure_hashfile_in_cwd(original_path)
# Call _ensure_hashfile_in_cwd
result_path = _ensure_hashfile_in_cwd(original_path)
# The result should be different from original (in cwd now)
# But original_path should still exist and be unchanged
assert os.path.exists(original_path), "Original file should still exist"
assert os.path.exists(result_path), "Result file should exist"
# The result should be different from original (in cwd now)
# But original_path should still exist and be unchanged
assert os.path.exists(original_path), "Original file should still exist"
assert os.path.exists(result_path), "Result file should exist"
# If they're different, result should be in cwd
if result_path != original_path:
assert os.path.dirname(result_path) == str(tmp_path), (
"Result should be in cwd"
)
finally:
os.chdir(orig_cwd)
# If they're different, result should be in cwd
if result_path != original_path:
assert os.path.dirname(result_path) == str(tmp_path), (
"Result should be in cwd"
)
if __name__ == "__main__":
+30 -9
View File
@@ -93,14 +93,14 @@ class TestEnsureHashfileInCwd:
result = main_module._ensure_hashfile_in_cwd("relative/path.txt")
assert result == "relative/path.txt"
def test_already_in_cwd(self, main_module, tmp_path):
def test_already_in_cwd(self, main_module, tmp_path, monkeypatch):
target = tmp_path / "hashfile.txt"
target.write_text("hashes")
with patch("os.getcwd", return_value=str(tmp_path)):
result = main_module._ensure_hashfile_in_cwd(str(target))
monkeypatch.setenv("HATE_CRACK_ORIG_CWD", str(tmp_path))
result = main_module._ensure_hashfile_in_cwd(str(target))
assert result == str(target)
def test_different_dir_existing_file_in_cwd(self, main_module, tmp_path):
def test_different_dir_existing_file_in_cwd(self, main_module, tmp_path, monkeypatch):
# File exists in a different directory
other_dir = tmp_path / "other"
other_dir.mkdir()
@@ -113,11 +113,11 @@ class TestEnsureHashfileInCwd:
cwd_copy = cwd_dir / "hashes.txt"
cwd_copy.write_text("cwd version")
with patch("os.getcwd", return_value=str(cwd_dir)):
result = main_module._ensure_hashfile_in_cwd(str(source_file))
monkeypatch.setenv("HATE_CRACK_ORIG_CWD", str(cwd_dir))
result = main_module._ensure_hashfile_in_cwd(str(source_file))
assert result == str(cwd_copy)
def test_different_dir_creates_symlink(self, main_module, tmp_path):
def test_different_dir_creates_symlink(self, main_module, tmp_path, monkeypatch):
# Source file in a different directory, nothing in cwd
other_dir = tmp_path / "other"
other_dir.mkdir()
@@ -127,13 +127,34 @@ class TestEnsureHashfileInCwd:
cwd_dir = tmp_path / "cwd"
cwd_dir.mkdir()
with patch("os.getcwd", return_value=str(cwd_dir)):
result = main_module._ensure_hashfile_in_cwd(str(source_file))
monkeypatch.setenv("HATE_CRACK_ORIG_CWD", str(cwd_dir))
result = main_module._ensure_hashfile_in_cwd(str(source_file))
expected = str(cwd_dir / "hashes.txt")
assert result == expected
assert os.path.exists(expected)
def test_uses_orig_cwd_not_process_cwd(self, main_module, tmp_path, monkeypatch):
"""Output files go to HATE_CRACK_ORIG_CWD, not the process CWD."""
# Simulate: process CWD is the install dir, orig CWD is the user's dir
install_dir = tmp_path / "install"
install_dir.mkdir()
user_dir = tmp_path / "user"
user_dir.mkdir()
other_dir = tmp_path / "other"
other_dir.mkdir()
source_file = other_dir / "hashes.txt"
source_file.write_text("hashes")
monkeypatch.chdir(install_dir)
monkeypatch.setenv("HATE_CRACK_ORIG_CWD", str(user_dir))
result = main_module._ensure_hashfile_in_cwd(str(source_file))
# Should land in user_dir, NOT install_dir
assert result == str(user_dir / "hashes.txt")
assert os.path.exists(user_dir / "hashes.txt")
assert not os.path.exists(install_dir / "hashes.txt")
class TestRunHashcatShow:
def _make_mock_result(self, stdout_bytes):
+12
View File
@@ -8,6 +8,18 @@ from hate_crack import cli
from hate_crack import formatting
def test_orig_cwd_uses_env_var(monkeypatch, tmp_path):
"""orig_cwd() returns HATE_CRACK_ORIG_CWD when set."""
monkeypatch.setenv("HATE_CRACK_ORIG_CWD", str(tmp_path))
assert cli.orig_cwd() == str(tmp_path)
def test_orig_cwd_falls_back_to_getcwd(monkeypatch):
"""orig_cwd() falls back to os.getcwd() when env var is unset."""
monkeypatch.delenv("HATE_CRACK_ORIG_CWD", raising=False)
assert cli.orig_cwd() == os.getcwd()
def test_resolve_path_none_and_expand():
assert cli.resolve_path("") is None
resolved = cli.resolve_path("~")