Home (All Topics) → Technology → Software → AI-Created Software
The following is AI-generated (aka vibe-coded) software. The software is specific to my needs and my system and may not work for you or your system. Support is limited, but if you report a bug I'll certainly look into it. All code is provided in a zip file for you to review should you choose.
Startup Ledger keeps a record of what starts with Windows, what VirusTotal says about it, and what you decided. It checks the file hashes in an Autoruns export against VirusTotal, remembers every answer, and on each run tells you what is new, what changed and what needs a look.
Get a VirusTotal API key. A free one is on your VirusTotal profile, under API key. Put it in the VT_API_KEY environment variable, which keeps it out of your shell history and the process list:
setx VT_API_KEY YOUR_KEY (Command Prompt, for new windows)
$env:VT_API_KEY = "YOUR_KEY" (PowerShell, this window)
You can also type it in the window’s Settings, or pass --virus-total-api-key. The window never saves it.
Get autorunsc (the command-line Autoruns, in the Autoruns download) and put it on the PATH, or give its place in Settings (--autorunsc-file-or-folder-path).
Export and check. Double-click startup-ledger.py (or run python startup-ledger.py) for the window, press Export from Autoruns, then Check with VirusTotal. On the command line, this exports to autoruns-export.csv and checks it:
python startup-ledger.py --export-autoruns TRUE --autorunsc-file-or-folder-path "C:\Tools\autorunsc64.exe"
The export asks for Administrator rights (a UAC prompt) if the app doesn’t have them: without them, autorunsc silently misses services and other users’ entries. An export you made yourself works too (--autoruns-export-file-path, or Browse in the window): run autorunsc64.exe -accepteula -nobanner -a * -c -h -s -o autoruns-export.csv.
The -s matters: without it the Signer column never says “(Verified)”, so --skip-trusted-signers TRUE and --prioritize TRUE can’t tell trusted files from the rest.
The free API allows 4 requests a minute and 500 a day. A typical PC has a few hundred distinct files, so the first run can take an hour or more and may need a second day; later runs only look up what’s new or due for a recheck. --skip-trusted-signers TRUE skips files with a verified Microsoft signature, which is most of them.
The export can run on a schedule, so a fresh one is waiting whenever you open the window, which imports the same file:
autoruns-export-partial-<number>.csv), and that file replaces the export only once it is complete and has entries. The window never reads half an export, and a failed run leaves the last good export in place.--check-with-virus-total FALSE only exports; leave it out to check the new export too (with VT_API_KEY set for the task, and --quiet TRUE for a shorter log).--add-timestamp TRUE keeps every export (autoruns-export-2026-10-08-192400.csv); the check reads the new one.A Task Scheduler action for it: program C:\Path\To\pythonw.exe (or python.exe), arguments "C:\Tools\startup-ledger\startup-ledger.py" --export-autoruns TRUE --check-with-virus-total FALSE --autorunsc-file-or-folder-path "C:\Tools\autorunsc64.exe" --autoruns-export-file-path "C:\Tools\startup-ledger\autoruns-export.csv".
Give the window the same export file (it remembers it), and it shows the newest export when it opens.
The export exits with 1 when it wasn’t written (autorunsc missing or failing, or an empty export), and the check then doesn’t run.
Run startup-ledger.py with no --autoruns-export-file-path (double-click it) and it opens a window:
autoruns-export.csv in the working folder), with the autorunsc in Settings, and shows autorunsc’s output beside the details. Import drops rows identical in every column, and shows each entry with its cached result, its allowlist state and what changed since the last results. Check runs the command line with the settings, shows its output as it goes (Stop ends it; every lookup is already cached), then shows the new results.VT_API_KEY). --allow and --disallow have none: Allow and Remove do the same for the selected rows.Check first marks the selected files to be looked up first in the next check, before every other file and even if VirusTotal was asked recently (or the vendor is trusted and skipped). The VirusTotal column says “check first”, and the details say since when. A file stays marked until a check has looked it up, so a check that stops early at the request limit or the quota gets to it next time; a lookup that fails keeps it marked too. Pressed again (it then reads Don’t check first), it unmarks them. Marks are kept in virus-total-priority.json.
Hash file works out the hash of the selected entries’ files now, reading each from this computer (in the background: a large file can take a moment). For an entry Autoruns left without a hash, because it couldn’t read the file or local hashing is off, it fills the hash in, so the row can be checked, allowlisted or marked to check first. For an entry that has one, the details say whether the file is unchanged since the export (or since the import hashed it) or has changed, with its new hash: export again to check the new file. A file that isn’t there is marked missing, as an import with local hashing marks it; one that is there but can’t be read is reported in red. The button is off for an entry that names no file (a command line). It changes only what the window shows; the next import or check reads the export again.
Only one window opens per working folder: launching it again brings that window to the front, with any options given filled in (and --export-autoruns TRUE exports in it). Checks on the command line still run while it’s open.
The window keeps its settings in startup-ledger.ini in its working folder (--working-folder-path; default: the folder holding startup-ledger.py), and relative file paths in the settings point there, so virus-total-results.csv, virus-total-cache.json, virus-total-allowlist.json and virus-total-first-seen.json sit beside the app. Settings from the app’s old name (check-autoruns-virustotal.ini) are taken over the first time.
| Key | What it does |
|---|---|
| F5 | Import the export again. |
| Ctrl+E | Export from Autoruns (not while typing in a field). |
| Ctrl+R | Check with VirusTotal (not while typing in a field). |
| Ctrl+F | Go to Search. |
| Esc | In Search: clear it. In Settings or a column filter: close it. |
| Enter | In the export field: import it. |
| Double-click a row | Open the file’s VirusTotal page. |
| Double-click a column heading’s right edge | Fit that column to its widest cell. |
python startup-ledger.py --autoruns-export-file-path EXPORT [options]
python startup-ledger.py --export-autoruns TRUE [--autoruns-export-file-path EXPORT] [options]
python startup-ledger.py --help
Every option takes a value, --name VALUE or --name=VALUE, in any order; yes/no options take TRUE or FALSE (or 1/0, YES/NO, ON/OFF, in any case). An option given twice, a bare --quiet or a value out of range is a mistake. Every option besides the export has a default.
Relative paths on the command line point to the current folder.
| Option | What it does |
|---|---|
--working-folder-path FOLDER |
The window’s folder for its settings and relative paths. Default: the app’s folder. |
--show-window TRUE / FALSE |
TRUE opens the window with these options filled in; FALSE stays on the command line. Default: the window when no export is given. |
--autoruns-export-file-path CSV |
The Autoruns export: the one checked, and the one --export-autoruns TRUE writes. Needed on the command line, unless exporting (then blank: autoruns-export.csv). |
--export-autoruns TRUE / FALSE |
TRUE first exports with autorunsc to the export file, replacing it once complete. Default: FALSE. |
--autorunsc-file-or-folder-path PATH |
autorunsc64.exe or autorunsc.exe, or the folder holding it. Blank: the first on the PATH. |
--add-timestamp TRUE / FALSE |
TRUE adds the date and time to the export’s file name, so earlier exports are kept. Default: FALSE. |
--elevate auto / ask / never |
Asking for Administrator rights for the export: auto from a console only, never in the background; ask always (the window’s Export); never warns and carries on. Default: auto. |
--check-with-virus-total TRUE / FALSE |
FALSE only exports (with --export-autoruns TRUE).Default: TRUE. |
--virus-total-api-key KEY |
Your API key. Default: the VT_API_KEY environment variable (better). |
--virus-total-output-file-path CSV |
The results. Default: virus-total-results.csv. |
--virus-total-cache-file-path JSON |
The resumable cache of lookups. Default: virus-total-cache.json. |
--virus-total-log-file-path LOG |
Also append everything printed to this file. |
--requests-per-minute N |
Throttle. Default: 4 (the free API’s limit). |
--max-retries N |
Retries for a lookup that fails with a server or network error. Default: 6. |
--max-requests N |
Most requests this run, retries included; 0: no limit. Default: 500. |
--recheck-older-than-days DAYS |
Look up again results older than this; 0: every one. Default: never. |
--recheck-not-found-days DAYS |
Look up again files VirusTotal didn’t know, once their result is this old. |
--recheck-flagged-days DAYS |
Look up again files that had detections, once their result is this old. |
--recheck-allowlisted-days DAYS |
Look up again allowlisted files, once their result is this old; none: never. Default: 7. |
--min-detections N |
Malicious detections that flag a file YES. Default: 1. |
--compare-to-file-path CSV |
A previous results file to compare with. Default: the last results (the output file, read before it’s replaced). |
--compare TRUE / FALSE |
FALSE doesn’t compare with a previous run. Default: TRUE. |
--local-hashing TRUE / FALSE |
FALSE doesn’t hash the files Autoruns left without a hash (use it for an export from another computer). Default: TRUE. |
--allow TARGETS |
Allowlist files in this export: comma-separated file names, paths, hashes or entry names. |
--disallow TARGETS |
Take files off the allowlist: the same kinds of targets. |
--allowlist-file-path JSON |
The allowlist. Default: virus-total-allowlist.json. |
--first-seen-file-path JSON |
When each entry (with its hash) was first reported. Default: virus-total-first-seen.json. |
--priority-file-path JSON |
The files marked in the window to check first; the check looks them up first and unmarks each once looked up. Default: virus-total-priority.json. |
--trusted-signers NAMES |
More trusted vendors for --skip-trusted-signers, comma-separated ("Intel,NVIDIA"; verified signatures only). |
--skip-trusted-signers TRUE / FALSE |
TRUE doesn’t look up files with a verified signature from a trusted vendor (Microsoft, and --trusted-signers). Default: FALSE. |
--quiet TRUE / FALSE |
TRUE hides clean or allowlisted results that were checked before. Default: FALSE. |
--prioritize TRUE / FALSE |
TRUE looks up the riskiest files first (unsigned, unverified, in user-writable folders), so they’re done before the quota runs out. Default: FALSE. |
Examples:
# Resume an interrupted run: run the same command again.
python startup-ledger.py --autoruns-export-file-path autoruns-export.csv
# Weekly: look again at the files VirusTotal didn't know and those with detections, cheaply.
python startup-ledger.py --autoruns-export-file-path autoruns-export.csv --recheck-not-found-days 7 --recheck-flagged-days 7
# Trust your own apps until they change, and stop trusting one.
python startup-ledger.py --autoruns-export-file-path autoruns-export.csv --allow "MyApp.exe,C:\Tools\backup-helper.exe"
python startup-ledger.py --autoruns-export-file-path autoruns-export.csv --disallow "backup-helper.exe"
# Export and check in one step, e.g. from Task Scheduler with "Run with highest privileges".
python startup-ledger.py --export-autoruns TRUE --autorunsc-file-or-folder-path "C:\Tools" --quiet TRUE
# A big machine: skip verified Microsoft files, riskiest first, only new or interesting lines.
python startup-ledger.py --autoruns-export-file-path autoruns-export.csv --skip-trusted-signers TRUE --prioritize TRUE --quiet TRUE
Exit status: 0 the export or check finished; 1 it couldn’t be done (no API key, no export, a failed export, a data file that can’t be used, results that couldn’t be written); 2 a command-line mistake (“Error: …” and “Use –help for usage.”), or a check that stopped early (a request limit, the quota, a rejected key, Ctrl+C). After a stop, run the same command again to continue.
Scripts and scheduled tasks written for the earlier command line need these changes; each old switch is now an option with a value:
| Before | Now |
|---|---|
--skip-trusted-signers, --prioritize, --quiet |
--skip-trusted-signers TRUE, --prioritize TRUE, --quiet TRUE |
--no-compare |
--compare FALSE |
--no-local-hashing |
--local-hashing FALSE |
--compare-to CSV |
--compare-to-file-path CSV |
--trusted-signer Intel --trusted-signer NVIDIA |
--trusted-signers "Intel,NVIDIA" |
--min-detections 0, --requests-per-minute 0: exit status 1 |
a command-line mistake, exit status 2 |
export-autoruns-hashes.py --autorunsc PATH --output PATH |
startup-ledger.py --export-autoruns TRUE --check-with-virus-total FALSE --autorunsc-file-or-folder-path PATH --autoruns-export-file-path PATH |
export script: --timestamp, --no-elevate |
--add-timestamp TRUE, --elevate never |
export script: --pause |
gone: autorunsc itself is run elevated, so no second window opens |
The window’s settings saved by an earlier version are kept, “Don’t compare” and “don’t hash files here” included.
--min-detections engines say malicious. REVIEW: fewer, or only suspicious ones; often a false positive, but worth a look.VT_Status = file_not_found): entries pointing to a file that doesn’t exist, usually leftovers of uninstalled software, sometimes malware remnants. No hash (no_hash): Autoruns couldn’t read the file; the app then hashes it itself (HashComputed = YES), the same hash VirusTotal uses. A file it looks for and doesn’t find is marked file_not_found too, whatever Autoruns said; one that is there but can’t be read (another program holds it), or in a folder this account can’t open, stays no_hash. A Known DLL that Autoruns lists by name only (wow64win.dll) is looked for in System32, so the ARM64 helpers (wowarmhw.dll, xtajit.dll) are missing on x64 Windows. That’s only right on the computer the export came from: --local-hashing FALSE turns it off.--min-detections say what the file is: its name, description, company, signature and the entries that start it. A file VirusTotal first saw in the last 30 days is pointed out.virus-total-results.csv has a row per entry, in UTF-8 with a byte-order mark so Excel shows accented characters. Any cell starting with =, +, - or @ gets a leading ', so Excel can’t run it as a formula: entry names and paths come from the machine being checked, which may be the compromised one.
For files you trust that VirusTotal will never know, such as your own apps:
--allow (or Allow in the window) records the file’s current hash and VirusTotal result in virus-total-allowlist.json. The entry stays out of the flagged, review and unknown lists while the file at that path still has that hash and VirusTotal says no worse than it did.atmfd.dll) can be acknowledged the same way, by the file’s name, its path or the entry’s name. An acknowledgement is for that entry and that path: Autoruns lists some Known DLLs twice under one name, for System32 and for SysWOW64, and each is acknowledged on its own. It’s alerted if a file appears at that path; an entry pointed at another missing file is a new missing file, not acknowledged until you allow it.--recheck-allowlisted-days (7 by default), so a change on VirusTotal is noticed. Each item has a note you can fill in by hand.
| File | What |
|---|---|
virus-total-results.csv |
The last results, also the next run’s comparison. |
virus-total-cache.json |
Every lookup, with when it was made. Saved after each one. |
virus-total-allowlist.json |
Your decisions. |
virus-total-first-seen.json |
When each entry was first reported. |
virus-total-priority.json |
The files marked to check first, until a check looks them up. |
startup-ledger.ini, startup-ledger.lock, tmp/ |
The window’s settings, its one-window lock, temporary files. |
Every file is written through a temporary file that replaces it in one step, so a crash never leaves half a file. A JSON file that isn’t valid is kept as <name>.damaged-<date and time>.json and the run starts without it. A file that can’t be opened (a sync client or antivirus holding it), or that holds JSON of another shape, stops the run before anything is looked up, and is left as it is: carrying on without it would replace your cache, allowlist or dates with the next save.
The tests use pytest, with VirusTotal and autorunsc faked, and every export built in the test:
python -m pytest # everything (the window tests start Xvfb when there's no display)
python -m pytest -m "not gui" # without opening windows
| File | What it covers |
|---|---|
tests/test_check.py |
Lookups and the cache, retries, an unreadable response, --max-requests, a rejected key and the quota, flags, formula-safe cells, trusted signers and --prioritize, files marked to check first, the allowlist and its alerts, the comparison, first-seen dates, local hashing, Known DLLs listed by name, locked, damaged and foreign data files, results that can’t be written, mistakes, the log, --quiet. |
tests/test_view.py |
The settings form and the command lines it makes (the check’s and the export’s), imports and their steps, a file hashed once, the table’s cells and colors, Show, search and column filters, the details, Allow and Remove, marking files to check first, hashing files again (filled in, unchanged, changed, missing), files an import finds missing, a missing Known DLL’s twin acknowledged, missing files told apart by path, the one-window lock. |
tests/test_command_line.py |
Every option, in order; choices written out for Scriptary; --help; running without Tkinter; a bad working folder; the export with a fake autorunsc: one-step writes, failed and empty exports, the default file name, --add-timestamp, export then check (nothing exported without a key), when Administrator rights are asked for; the icon maker writing every size. |
tests/test_window.py |
Importing, a bad export, search, Show and column filters with the sort and selection, the filter checklist, column widths fitted, then kept as you set them, the settings’ labels beside their fields, Allow and Remove, Check first, Hash file, a check and an export run from the window, importing again while a filter hides rows, a long import’s progress, an error shown in the window, settings saved (never the key), hand-edited and saved by the old switches, main() with --working-folder-path, palette colors, closing. |
The 75 tests take about 12 seconds, and pass at 100% and at 150% display scaling (Xvfb :98 -screen 0 3000x1800x24 -dpi 144, then DISPLAY=:98 python -m pytest).
The refactor that split the app into a package was checked against the single-file version it replaced: eleven runs in a row on one export with a fake VirusTotal (a first run stopped by --max-requests, a resume with --allow, --quiet and rechecks, worse verdicts, flaky network, --disallow, a rejected key, the quota, a log file and mistakes) gave the same console output, results CSV, cache, allowlist and first-seen file, timestamps aside.
Measured under Xvfb on the cloud test machine: a search keystroke with 3,000 entries refreshes the table in 65 ms (159 ms before the refactor; 24 ms instead of 78 with 1,000), since the rows are put in the table once per import and filtering only chooses which are shown.--help takes 102 ms instead of 182, since the command line no longer loads Tk.
“No VirusTotal API key”: set VT_API_KEY, or pass --virus-total-api-key. In the window, type it in Settings. A window opened before you ran setx doesn’t see the new variable.
“No verified signatures found in this export”: the export was made without -s. Export again with Export from Autoruns or --export-autoruns TRUE.
The run stops with “most likely the daily or monthly quota”: the free key’s 500 requests a day are used up. Run the same command tomorrow; every lookup so far is cached.
“Cannot open … virus-total-cache.json” (or the allowlist): another program has the file open. Close it (or pause the sync client) and run again.
“Could not write the results”: virus-total-results.csv is probably open in Excel. Close it and run again; nothing is looked up twice.
The window says it’s already open: one window per working folder; the open one comes to the front. If it didn’t answer, a window may be stuck: close it, or end its Python process.
The export has far fewer entries than Autoruns shows: the export ran without Administrator rights. Run it from an elevated prompt, or with “Run with highest privileges” in Task Scheduler.
“Could not find autorunsc64.exe or autorunsc.exe on the PATH”: give its file or folder in Settings (autorunsc64.exe) or with --autorunsc-file-or-folder-path.
Import takes a long time: the status bar names the step. “Hashing the files Autoruns couldn’t read” reads each of those files on this computer, which takes a while for large ones or many; the window remembers them, so the next import is quick. An export made with Administrator rights leaves few to hash.--local-hashing FALSE (in Settings) turns it off.
“Something went wrong” in the status bar: a mistake in the app; the details are in the output panel. Please report them.
“Error: –quiet needs a value” (or another yes/no option): the option takes TRUE or FALSE now; see Options that changed.
startup-ledger.py is a small launcher holding the command line (make_parser, main); the app is the startup_ledger package beside it. Everything but the window runs without Tk.
| Module | What |
|---|---|
autoruns.py |
Reading an export: encodings, duplicates, the hash column, hashing files Autoruns couldn’t, signers and risk. |
virustotal.py |
The lookup: throttle, request budget, retries and backoff, what each response means. |
recheck.py |
When a cached result is looked up again. |
allowlist.py |
The allowlist’s keys, states, baselines, and --allow / --disallow matching. |
results.py |
Merging the export with the results, the comparison with the last run, first-seen dates, the results CSV. |
datafiles.py |
Reading and writing the cache, allowlist and first-seen files safely. |
export.py |
The export with autorunsc: finding it, Administrator rights, the one-step replace. |
check.py |
A check on the command line, step by step. |
report.py |
The console report and summary. |
cli.py |
Option types, the log file, window or command line, export then check. |
view.py |
The window’s logic without Tk: the form, imports, cells, filters, details. |
window.py, window_lock.py |
The window, and one window per working folder. |
working_folder.py, command_line.py, gui_base.py, light_theme.py |
Copies of the app templates’ shared code. |
The icon, startup-ledger.ico beside the launcher (a magnifying glass over a power symbol), is drawn by tools/make_icon.py at each size Windows uses, 16 to 256 pixels; sizes of 32 and under get a larger glass and heavier strokes so they stay clear on the taskbar. Run python tools/make_icon.py after changing the drawing (it needs Pillow; --png-folder-path writes each size as a PNG to look at). On Windows the window uses the .ico as it is; elsewhere it needs Pillow with ImageTk to show it.


Leave a Reply