diff --git a/samples/vboxwrapper/vbox.h b/samples/vboxwrapper/vbox.h
index 67a1b255f476b1f924715f06b6c839c758101dec..db748b845da82391d2346789fe13eca96e54bb1a 100644
--- a/samples/vboxwrapper/vbox.h
+++ b/samples/vboxwrapper/vbox.h
@@ -140,8 +140,15 @@ public:
     double minimum_checkpoint_interval;
         // minimum time between checkpoints
     std::vector<std::string> copy_to_shared;
+        // list of files to copy from slot dir to shared/
     std::vector<std::string> trickle_trigger_files;
-    std::vector<std::string> completion_trigger_files;
+        // if find file of this name in shared/, send trickle-up message
+        // with variety = filename, contents = file contents
+    std::string completion_trigger_file;
+        // if find this file in shared/, task is over.
+        // File can optionally contain exit code (first line)
+        // and stderr text (subsequent lines).
+        // Addresses a problem where VM doesn't shut down properly
 
     /////////// END VBOX_JOB.XML ITEMS //////////////
 
@@ -177,6 +184,7 @@ public:
     int pause();
     int resume();
     void check_trickle_triggers();
+    void check_completion_trigger();
     int createsnapshot(double elapsed_time);
     int cleanupsnapshots(bool delete_active);
     int restoresnapshot();
diff --git a/samples/vboxwrapper/vboxwrapper.cpp b/samples/vboxwrapper/vboxwrapper.cpp
index 3183eb28c8935f043374a56511191d94c3b9ccc2..2e20b29c7409466c3d47eec335b0cabd6adceb53 100644
--- a/samples/vboxwrapper/vboxwrapper.cpp
+++ b/samples/vboxwrapper/vboxwrapper.cpp
@@ -176,7 +176,7 @@ int parse_job_file(VBOX_VM& vm) {
             continue;
         }
         else if (xp.parse_string("completion_trigger_file", str)) {
-            vm.completion_trigger_files.push_back(str);
+            vm.completion_trigger_file = str;
             continue;
         }
         fprintf(stderr, "%s parse_job_file(): unexpected tag %s\n",
@@ -380,25 +380,28 @@ void set_remote_desktop_info(APP_INIT_DATA& /* aid */, VBOX_VM& vm) {
     }
 }
 
-// check for trickle trigger files, and send trickles if find them.
+// check for completion trigger file
 //
-void VBOX_VM::check_trickle_triggers() {
-    char filename[256], path[MAXPATHLEN], buf[256];
-    for (unsigned int i=0; i<trickle_trigger_files.size(); i++) {
-        strcpy(filename, trickle_trigger_files[i].c_str());
-        sprintf(path, "shared/%s", filename);
-        if (!boinc_file_exists(path)) continue;
-        string text;
-        int retval = read_file_string(path, text);
-        if (retval) {
-            fprintf(stderr,
-                "%s can't read trickle trigger file %s\n",
-                vboxwrapper_msg_prefix(buf, sizeof(buf)), filename
-            );
+void VBOX_VM::check_completion_trigger() {
+    char path[MAXPATHLEN];
+
+    sprintf(path, "shared/%s", completion_trigger_file.c_str());
+    if (!boinc_file_exists(path)) return;
+    int exit_code = 0;
+    FILE* f = fopen(path, "r");
+    if (f) {
+        char buf[1024];
+        if (fgets(buf, 1024, f) != NULL) {
+            exit_code = atoi(buf);
         }
-        boinc_send_trickle_up(filename, const_cast<char*>(text.c_str()));
-        boinc_delete_file(path);
+        while (fgets(buf, 1024, f) != NULL) {
+            fputs(buf, stderr);
+        }
+        fclose(f);
     }
+    cleanup();
+    dumphypervisorlogs(true);
+    boinc_finish(exit_code);
 }
 
 // check for trickle trigger files, and send trickles if find them.
@@ -1072,6 +1075,9 @@ int main(int argc, char** argv) {
             if ((loop_iteration % 10) == 0) {
                 current_cpu_time = starting_cpu_time + vm.get_vm_cpu_time();
                 vm.check_trickle_triggers();
+                if (!vm.completion_trigger_file.empty()) {
+                    vm.check_completion_trigger();
+                }
             }
 
             if (vm.job_duration) {