Dev Investigation Notes

When a hidden-VBS Python script in Startup occasionally pops an error dialog and silently fails to launch only at boot

This is a translated article. Read the original in Korean ↗

I was building a local voice assistant (voice-assistant) for Windows that stays running and answers when it hears a wake word. To get it to start without a window, I put a hidden VBS in the Startup folder that launched Python (pythonw). Every so often a VBS error dialog popped up, and on some days the assistant didn't start at all after boot. Nothing showed up in the logs.

Conclusion

On Error Resume Next hides errors . The cause of the repeated wscript failures is also still unconfirmed . Keep the delay and retry settings .

Run pythonw.exe directly instead of going through a batch file or VBS. The Hidden setting hides the task from the task list. It does not hide a cmd window. pythonw doesn't create a console in the first place.

For a voice assistant, choose your own account and ‘사용자가 로그온할 때만 실행’ ("Run only when user is logged on") (Interactive). If you pick ‘로그온 여부와 관계없이’ ("Run whether user is logged on or not") to avoid the window, the task also runs in a different interactive session (logon type).

Below is an unverified example of how to apply this. Change the paths and module, and move any environment setup and file logging from the batch file into the app.

$user = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$action = New-ScheduledTaskAction -Execute "C:\myapp\.venv\Scripts\pythonw.exe" -Argument "-m src.main" -WorkingDirectory "C:\myapp"
$principal = New-ScheduledTaskPrincipal -UserId $user -LogonType Interactive
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $user
$trigger.Delay = "PT45S"
$settings = New-ScheduledTaskSettingsSet -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -MultipleInstances IgnoreNew
Register-ScheduledTask -TaskName "MyAssistant" -Action $action -Trigger $trigger -Settings $settings -Principal $principal
# Single-instance guard example - a second process that starts up exits on its own
import ctypes, sys
ERROR_ALREADY_EXISTS = 183
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
_mutex = kernel32.CreateMutexW(None, False, "Global\\MyAssistantSingleton")  # keep the reference alive until the process exits
if ctypes.get_last_error() == ERROR_ALREADY_EXISTS:
    print("이미 실행 중인 인스턴스가 있어 종료함")
    sys.exit(0)

(The printed message means "An instance is already running, exiting.")

Situation

The whole voice assistant runs on the local PC: wake-word detection, then STT (speech-to-text), then an LLM call (Claude Code CLI), then speech synthesis (TTS). Responses come from running claude -p as a child process . Python stays resident as the venv's pythonw.exe, the Python build that has no console window .

The auto-start chain looked like this:

시작 프로그램 폴더의 VoiceAssistant.lnk
  -> wscript.exe start_assistant_hidden.vbs   (WshShell.Run, 창 스타일 0 = 숨김)
    -> 배치 파일
      -> pythonw.exe

(In English: VoiceAssistant.lnk in the Startup folder → wscript.exe start_assistant_hidden.vbs (WshShell.Run, window style 0 = hidden) → batch file → pythonw.exe.)

The input device is a USB audio interface, and on startup the app looks the device up by name .

Symptom

  1. An occasional VBS error dialog. An error alert about "hidden_vbs 무슨 파일" ("some hidden_vbs file") popped up now and then. The error text was never recorded .
  2. No assistant after boot. The shortcut was still in the Startup folder, but not a single pythonw.exe was running . It hadn't been disabled in Task Manager either (no disabled entry in the registry) .
  3. The log was silent. The PC booted at 22:33, and after that main_log.txt didn't gain a single line. It looked like the app never started, not like it started and then crashed .
  4. And yet it worked fine when run by hand. Running it directly with python.exe got all the way to "준비 완료" ("ready"), and running the batch file directly also brought it up .

Cause

Not confirmed. Here's what was confirmed, kept separate from what's only a guess.

Confirmed:

Guesses (unverified):

A caveat: earlier the same day, in the early morning, while investigating the same symptom after an earlier boot, the shell used for the investigation cleaned up the child processes it had launched once each call finished. That mixed fake "starts, then disappears" failures into the results . When the app was launched outside that shell (via WMI), it stayed up just fine . The "four failures in a row" above were also run from the same kind of tool shell, so it can't be fully ruled out that the same effect got mixed in. If you're testing reproduction from an automation tool or a remote shell, first rule out that the shell itself is killing your processes.

Fix

Without knowing the cause, I moved the launch to a more robust setup.

Before: hiding the symptom. I put On Error Resume Next at the top of both VBS files to stop the error dialog . The dialog went away, but the cause stayed unknown. A few days later, chasing the boot failure, the first thing I had to suspect was "is this swallowing the error?" , and as long as that line stayed in, I couldn't see errors even by running it with cscript.

After: replacing how it's launched

  1. Registered a Task Scheduler task, VoiceAssistantJarvis: runs 45 seconds after logon, retries up to 3 times at 1-minute intervals on failure, ignores a new run if one is already running. It bypasses the silently failing wscript chain and runs the batch file directly. Instead of deleting the Startup folder shortcut, I moved it to scripts/_disabled_startup/ to prevent a double launch .
  2. Made the app hold up better at boot. On startup it prints [main] === 시작 <타임스탬프> (pid ...) === (시작 <타임스탬프> = "start <timestamp>"). If it fails to come up again, the log alone tells you whether it even attempted to start. The input stream retries for up to 60 seconds until the device is ready . Launched from the scheduler, this startup log line actually got written .
  3. Blocked a second instance with a mutex. When I deliberately launched one more, it printed "이미 실행 중인 인스턴스가 있어 종료함" ("An instance is already running, exiting") and quit immediately .

What was verified: running the task on demand confirmed startup, logging, and the mutex. There is no record of confirming it with an actual reboot .

Bonus — two pythonw processes running is not a duplicate launch

Task Manager showed two pythonw.exe per app, which made me suspect a duplicate launch. Checking the parent PIDs and CPU showed the parent was the venv's launcher stub (CPU 0), and one child process was doing the actual work (CPU 79.9%) . To shut it down, filter by command line (CommandLine) rather than by a single PID, and kill both . To prevent real duplicates, use the mutex shown above.

Reporter's Notes — once the error dialog was off, there was nothing to see

Park Doeun
You know the voice assistant on Windows that stays running and answers when you say "Hey Jarvis"? I turned on my computer today and it's not there. The shortcut is still in the Startup folder, but there's not a single pythonw running
Jung Haneul
First let's figure out whether it crashed or never started. Compare the boot time with the timestamp of the last line in the log.
Park Doeun
Boot was at 22:33, and main_log.txt hasn't gained a single line since. It never started
Jung Bada
Last time we put On Error Resume Next at the top to stop the VBS error dialog. That tells it to ignore errors and move on to the next line. Isn't that swallowing the real error?
Jung Haneul
Then let's run it with cscript instead of wscript. Same script host, but it prints errors to the console instead of a dialog.
Park Doeun
Ran it with cscript: no error, exit 0. But pythonw still doesn't start
Jung Bada
No error means the VBS is fine. So On Error Resume Next isn't the culprit.
Jung Haneul
You can't conclude that. If that line is still in the script, errors slip past silently in cscript too. Exit 0 could mean "there was no error" or "there was an error and we couldn't see it."
Park Dohyun
That's the cost of that one line. The moment you turned off the error dialog, you also turned off the clue that tells you what failed. That's why even pulling out cscript showed you nothing.
Jung Bada
Then maybe hidden mode is the problem. The VBS uses window style 0, the option that launches without showing a window.
Park Doeun
With style 1, so the window shows, it started. But when I set it back to 0, it started that time too
Jung Haneul
One run each won't settle it. Run the VBS through wscript several times, same as at boot, and count the success rate.
Park Doeun
Four failures in a row. Zero processes, zero bytes added to the log. Running the batch file directly brings it up every time
Jung Bada
There we go, it reproduced, so the wscript path is the culprit.
Park Doeun
Wait, the very next run came up with the same wscript. The log got all the way to "준비 완료" ("ready")
Jung Haneul
If the same path works sometimes and fails other times, it's nondeterministic. The log was 0 bytes on failure, so not one line of Python ran. We don't have the evidence to pin down the cause here.
Jung Bada
Right after boot, antivirus scans are running and the USB audio device might not be detected yet. And there's code that kills the app if it can't find the device by name
Jung Haneul
That's circumstantial. If it died at the device step, there should've been at least some log output before that, but it was 0 bytes. Let's leave it as a guess.
Park Doeun
One more thing bugs me. When we first investigated early this morning, the shell we were testing from cleaned up the processes it had launched once each command finished, so things that were fine looked dead. Those four failures just now were run from the same kind of shell, so I can't promise that didn't affect them
Park Dohyun
Which is why changing how it's launched beats digging for the cause. Even without knowing why, a setup that "starts late, retries on failure, and shuts one down if two come up" holds up no matter what the cause is.
Jung Haneul
Let's register it in Task Scheduler. Drop the silently failing wscript chain and run the batch file directly. Run 45 seconds after logon, retry up to three times at 1-minute intervals on failure, ignore a new run if it's already up
Park Doeun
I put some of that in the app too. It prints a timestamp and PID on startup, and waits up to 60 seconds for the input device. Ran it on demand and got [main] === 시작 ... (pid 18320) === ("start")
Jung Haneul
Let's check the two-instances case too. With a mutex, meaning a named lock managed by the OS, only whichever process grabs it first survives. So launch one more.
Park Doeun
The second one printed "이미 실행 중인 인스턴스가 있어 종료함" ("An instance is already running, exiting") and shut down right away
Park Dohyun
On Error Resume Next is a switch that turns off the error dialog, not a tool that fixes the cause. If you can't pin down the cause, don't hide the errors. Move to a launch setup that leaves a trace when it fails and gets itself back up.