I've used your patch and after adding
<pre>
    http_exchange(&send, &recv, cloneFlag==0 || nCycle>0);
    blob_reset(&send);
</pre>
to the client code and making the cmd array in the hook function larger, it works.

But some remarks:
  *  The server process waits until the end of the hook. I don't think, that is is really a good thing
  *  simply appending outputs to a file may become tricky, if multiple client request come in at the same time. The parallel running hook jobs will append their data at the same time and will mess up the logs

The idea of sending a special 'do-it'-line from the client is nice. The whole thing might be extended - i know, your patch is not finished :-) - with:
  *  the hook command should be configured by a setting (gui and/or command line setting)
  *  redirects of stdin/stderr should be part of the command configuration
  *  the client could use a special settings string, which would be appended to the 'do-it'-line. This setting could be read by the server and added to the command line of the server. This way, the users of such hooks can configure the message passing during the sync. Given your patch, the client might send <kbd>#FAT build</kbd> and the server would call <kbd>targetcmd build</kbd>
  *  if both sides of the line use a setting for the 'do-it' string (FAT in your patch), it would be fully configurable, if the clients wants to send a 'do-it' line and/or the server recognizes these requests.

best regards<br>

The results of my tests:
<nowiki><pre>
Index: src/xfer.c
===================================================================
--- src/xfer.c
+++ src/xfer.c
@@ -40,10 +40,26 @@
   int nDeltaRcvd;     /* Number of deltas received */
   int nDanglingFile;  /* Number of dangling deltas received */
   int mxSend;         /* Stop sending "file" with pOut reaches this size */
 };

+/*
+** Let a server-side external agent know that a push has completed. /fatman
+*/
+void post_push_hook() {
+  /*
+  ** TO DO: get the string cmd from a config file? Or the database local
+  ** settings, as someone suggested? Ditto output and error logs. /fatman
+  */
+  char cmd[1024] = "targetcmd";
+  strcat(cmd, " >target.out 2>target.err");
+  const char *ptr = cmd;
+  int rc = system(ptr);
+  if (rc != 0) {
+    fossil_print("The post-push-hook command failed.", ptr);
+  }
+}

 /*
 ** The input blob contains a UUID.  Convert it into a record ID.
 ** Create a phantom record if no prior record exists and
 ** phantomize is true.
@@ -617,11 +633,17 @@
   );
   zNow = db_text(0, "SELECT strftime('%%Y-%%m-%%dT%%H:%%M:%%S', 'now')");
   @ # timestamp %s(zNow)
   manifest_crosslink_begin();
   while( blob_line(xfer.pIn, &xfer.line) ){
-    if( blob_buffer(&xfer.line)[0]=='#' ) continue;
+    if( blob_buffer(&xfer.line)[0]=='#' ){
+      if ( 0 == memcmp(blob_buffer(&xfer.line), "#FAT", 4) ){
+        fprintf(stderr,"HOOK\n");
+       post_push_hook();
+      }
+      continue;
+    }
     xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, count(xfer.aToken));

     /*   file UUID SIZE \n CONTENT
     **   file UUID DELTASRC SIZE \n CONTENT
     **
@@ -1330,14 +1352,20 @@
     }

     /* If this is a clone, the go at least two rounds */
     if( cloneFlag && nCycle==1 ) go = 1;
   };
+  if (pushFlag && nFileSend > 0) {
+    blob_appendf(&send, "#FAT\n");
+    http_exchange(&send, &recv, cloneFlag==0 || nCycle>0);
+    blob_reset(&send);
+    nCardSent++;
+  }
   transport_stats(&nSent, &nRcvd, 1);
   fossil_print("Total network traffic: %d bytes sent, %d bytes received\n",
                nSent, nRcvd);
   transport_close();
   transport_global_shutdown();
   db_multi_exec("DROP TABLE onremote");
   manifest_crosslink_end();
   db_end_transaction(0);
 }