Index: src/db.c
==================================================================
--- src/db.c
+++ src/db.c
@@ -773,11 +773,25 @@
 ** case, invoke this routine with useAttach as 1.
 */
 void db_open_config(int useAttach){
   char *zDbName;
   const char *zHome;
-  if( g.configOpen ) return;
+  if( g.configOpen ){
+    if( useAttach==g.useAttach ) return;
+    if( g.useAttach ){
+      db_detach("configdb");
+      g.useAttach = 0;
+    }else if( g.dbConfig ){
+      sqlite3_close(g.dbConfig);
+      g.dbConfig = 0;
+      g.zConfigDbType = 0;
+    }else if( g.db ){
+      sqlite3_close(g.db);
+      g.db = 0;
+      g.zMainDbType = 0;
+    }
+  }
 #if defined(_WIN32)
   zHome = fossil_getenv("LOCALAPPDATA");
   if( zHome==0 ){
     zHome = fossil_getenv("APPDATA");
     if( zHome==0 ){

Index: src/main.c
==================================================================
--- src/main.c
+++ src/main.c
@@ -198,10 +198,11 @@
   char zCsrfToken[12];    /* Value of the anti-CSRF token */
   int okCsrf;             /* Anti-CSRF token is present and valid */
 
   int parseCnt[10];       /* Counts of artifacts parsed */
   FILE *fDebug;           /* Write debug information here, if the file exists */
+  int fNoThHook;          /* Disable all TH1 command/webpage hooks */
   int thTrace;            /* True to enable TH1 debugging output */
   Blob thLog;             /* Text of the TH1 debugging output */
 
   int isHome;             /* True if rendering the "home" page */
 
@@ -513,10 +514,11 @@
     g.fSystemTrace = find_option("systemtrace", 0, 0)!=0;
     g.fSshTrace = find_option("sshtrace", 0, 0)!=0;
     if( g.fSqlTrace ) g.fSqlStats = 1;
     g.fSqlPrint = find_option("sqlprint", 0, 0)!=0;
     g.fHttpTrace = find_option("httptrace", 0, 0)!=0;
+    g.fNoThHook = find_option("no-th-hook", 0, 0)!=0;
     g.zLogin = find_option("user", "U", 1);
     g.zSSLIdentity = find_option("ssl-identity", 0, 1);
     if( find_option("utc",0,0) ) g.fTimeFormat = 1;
     if( find_option("localtime",0,0) ) g.fTimeFormat = 2;
     if( zChdir && chdir(zChdir) ){
@@ -536,13 +538,26 @@
     }
     zCmdName = g.argv[1];
   }
   rc = name_search(zCmdName, aCommand, count(aCommand), &idx);
   if( rc==1 ){
-    fossil_fatal("%s: unknown command: %s\n"
-                 "%s: use \"help\" for more information\n",
-                   g.argv[0], zCmdName, g.argv[0]);
+    if( !g.isHTTP && !g.fNoThHook ){
+      rc = Th_CommandHook(zCmdName, 0);
+    }else{
+      rc = TH_OK;
+    }
+    if( rc==TH_OK || rc==TH_RETURN || rc==TH_CONTINUE ){
+      if( rc==TH_OK || rc==TH_RETURN ){
+        fossil_fatal("%s: unknown command: %s\n"
+                     "%s: use \"help\" for more information\n",
+                     g.argv[0], zCmdName, g.argv[0]);
+      }
+      if( !g.isHTTP && !g.fNoThHook && (rc==TH_OK || rc==TH_CONTINUE) ){
+        Th_CommandNotify(zCmdName, 0);
+      }
+    }
+    fossil_exit(0);
   }else if( rc==2 ){
     int i, n;
     Blob couldbe;
     blob_zero(&couldbe);
     n = strlen(zCmdName);
@@ -556,11 +571,36 @@
                  "%s: use \"help\" for more information\n",
                  g.argv[0], zCmdName, g.argv[0], blob_str(&couldbe), g.argv[0]);
     fossil_exit(1);
   }
   atexit( fossil_atexit );
-  aCommand[idx].xFunc();
+  /*
+  ** The TH1 return codes from the hook will be handled as follows:
+  **
+  ** TH_OK: The xFunc() and the TH1 notification will both be executed.
+  **
+  ** TH_ERROR: The xFunc() and the TH1 notification will both be skipped.
+  **
+  ** TH_BREAK: The xFunc() and the TH1 notification will both be skipped.
+  **
+  ** TH_RETURN: The xFunc() will be executed, the TH1 notification will be
+  **            skipped.
+  **
+  ** TH_CONTINUE: The xFunc() will be skipped, the TH1 notification will be
+  **              executed.
+  */
+  if( !g.isHTTP && !g.fNoThHook ){
+    rc = Th_CommandHook(aCommand[idx].zName, aCommand[idx].cmdFlags);
+  }else{
+    rc = TH_OK;
+  }
+  if( rc==TH_OK || rc==TH_RETURN || rc==TH_CONTINUE ){
+    if( rc==TH_OK || rc==TH_RETURN ){ aCommand[idx].xFunc(); }
+    if( !g.isHTTP && !g.fNoThHook && (rc==TH_OK || rc==TH_CONTINUE) ){
+      Th_CommandNotify(aCommand[idx].zName, aCommand[idx].cmdFlags);
+    }
+  }
   fossil_exit(0);
   /*NOT_REACHED*/
   return 0;
 }
 
@@ -1508,11 +1548,37 @@
       @ <h1>Server Configuration Error</h1>
       @ <p>The database schema on the server is out-of-date.  Please ask
       @ the administrator to run <b>fossil rebuild</b>.</p>
     }
   }else{
-    aWebpage[idx].xFunc();
+    /*
+    ** The TH1 return codes from the hook will be handled as follows:
+    **
+    ** TH_OK: The xFunc() and the TH1 notification will both be executed.
+    **
+    ** TH_ERROR: The xFunc() and the TH1 notification will both be skipped.
+    **
+    ** TH_BREAK: The xFunc() and the TH1 notification will both be skipped.
+    **
+    ** TH_RETURN: The xFunc() will be executed, the TH1 notification will be
+    **            skipped.
+    **
+    ** TH_CONTINUE: The xFunc() will be skipped, the TH1 notification will be
+    **              executed.
+    */
+    int rc;
+    if( !g.isHTTP && !g.fNoThHook ){
+      rc = Th_WebpageHook(aWebpage[idx].zName, aWebpage[idx].cmdFlags);
+    }else{
+      rc = TH_OK;
+    }
+    if( rc==TH_OK || rc==TH_RETURN || rc==TH_CONTINUE ){
+      if( rc==TH_OK || rc==TH_RETURN ){ aWebpage[idx].xFunc(); }
+      if( !g.isHTTP && !g.fNoThHook && (rc==TH_OK || rc==TH_CONTINUE) ){
+        Th_WebpageNotify(aWebpage[idx].zName, aWebpage[idx].cmdFlags);
+      }
+    }
   }
 
   /* Return the result.
   */
   cgi_reply();

Index: src/th_main.c
==================================================================
--- src/th_main.c
+++ src/th_main.c
@@ -20,10 +20,19 @@
 */
 #include "config.h"
 #include "th_main.h"
 #include "sqlite3.h"
 
+/*
+** These are the "well-known" TH1 error messages that occur when no hook is
+** registered to be called prior to executing a command or processing a web
+** page, respectively.  If one of these errors is seen, it will not be sent
+** or displayed to the remote user or local interactive user, respectively.
+*/
+#define NO_COMMAND_HOOK_ERROR "no such command:  command_hook"
+#define NO_WEBPAGE_HOOK_ERROR "no such command:  webpage_hook"
+
 /*
 ** Global variable counting the number of outstanding calls to malloc()
 ** made by the th1 implementation. This is used to catch memory leaks
 ** in the interpreter. Obviously, it also means th1 is not threadsafe.
 */
@@ -856,10 +865,136 @@
     if( z[0]!='>' ) return 0;
     i += 2;
   }
   return i;
 }
+
+/*
+** This function is called by Fossil just prior to dispatching a command.
+** Returning a value other than TH_OK from this function (i.e. via an
+** evaluated script raising an error or calling [break]/[continue]) will
+** cause the actual command execution to be skipped.
+*/
+int Th_CommandHook(
+  const char *zName,
+  char cmdFlags
+){
+  int rc = TH_OK;
+  Th_FossilInit(1, 1);
+  Th_Store("cmd_name", zName);
+  Th_StoreInt("cmd_flags", cmdFlags);
+  rc = Th_Eval(g.interp, 0, "command_hook", -1);
+  if( rc==TH_ERROR ){
+    int nResult = 0;
+    char *zResult = (char*)Th_GetResult(g.interp, &nResult);
+    /*
+    ** Make sure that the TH1 script error was not caused by a "missing"
+    ** command hook handler as that is not actually an error condition.
+    */
+    if( memcmp(zResult, NO_COMMAND_HOOK_ERROR, nResult)!=0 ){
+      sendError(zResult, nResult, 0);
+    }
+  }
+  /*
+  ** If the script returned TH_ERROR (e.g. the "command_hook" TH1 command does
+  ** not exist because commands are not being hooked), return TH_OK because we
+  ** do not want to skip executing essential commands unless the called command
+  ** (i.e. "command_hook") explicitly forbids this by successfully returning
+  ** TH_BREAK or TH_CONTINUE.
+  */
+  if( g.thTrace ){
+    Th_Trace("[command_hook {%h}] => %h<br />\n", zName,
+             Th_ReturnCodeName(rc));
+  }
+  return (rc != TH_ERROR) ? rc : TH_OK;
+}
+
+/*
+** This function is called by Fossil just after dispatching a command.
+** Returning a value other than TH_OK from this function (i.e. via an
+** evaluated script raising an error or calling [break]/[continue]) may
+** cause an error message to be displayed to the local interactive user.
+** Currently, TH1 error messages generated by this function are ignored.
+*/
+int Th_CommandNotify(
+  const char *zName,
+  char cmdFlags
+){
+  int rc;
+  Th_FossilInit(1, 1);
+  Th_Store("cmd_name", zName);
+  Th_StoreInt("cmd_flags", cmdFlags);
+  rc = Th_Eval(g.interp, 0, "command_notify", -1);
+  if( g.thTrace ){
+    Th_Trace("[command_notify {%h}] => %h<br />\n", zName,
+             Th_ReturnCodeName(rc));
+  }
+  return rc;
+}
+
+/*
+** This function is called by Fossil just prior to processing a web page.
+** Returning a value other than TH_OK from this function (i.e. via an
+** evaluated script raising an error or calling [break]/[continue]) will
+** cause the actual web page processing to be skipped.
+*/
+int Th_WebpageHook(
+  const char *zName,
+  char cmdFlags
+){
+  int rc = TH_OK;
+  Th_FossilInit(1, 1);
+  Th_Store("web_name", zName);
+  Th_StoreInt("web_flags", cmdFlags);
+  rc = Th_Eval(g.interp, 0, "webpage_hook", -1);
+  if( rc==TH_ERROR ){
+    int nResult = 0;
+    char *zResult = (char*)Th_GetResult(g.interp, &nResult);
+    /*
+    ** Make sure that the TH1 script error was not caused by a "missing"
+    ** webpage hook handler as that is not actually an error condition.
+    */
+    if( memcmp(zResult, NO_WEBPAGE_HOOK_ERROR, nResult)!=0 ){
+      sendError(zResult, nResult, 1);
+    }
+  }
+  /*
+  ** If the script returned TH_ERROR (e.g. the "webpage_hook" TH1 command does
+  ** not exist because commands are not being hooked), return TH_OK because we
+  ** do not want to skip processing essential web pages unless the called
+  ** command (i.e. "webpage_hook") explicitly forbids this by successfully
+  ** returning TH_BREAK or TH_CONTINUE.
+  */
+  if( g.thTrace ){
+    Th_Trace("[webpage_hook {%h}] => %h<br />\n", zName,
+             Th_ReturnCodeName(rc));
+  }
+  return (rc != TH_ERROR) ? rc : TH_OK;
+}
+
+/*
+** This function is called by Fossil just after processing a web page.
+** Returning a value other than TH_OK from this function (i.e. via an
+** evaluated script raising an error or calling [break]/[continue]) may
+** cause an error message to be displayed to the remote user.
+** Currently, TH1 error messages generated by this function are ignored.
+*/
+int Th_WebpageNotify(
+  const char *zName,
+  char cmdFlags
+){
+  int rc;
+  Th_FossilInit(1, 1);
+  Th_Store("web_name", zName);
+  Th_StoreInt("web_flags", cmdFlags);
+  rc = Th_Eval(g.interp, 0, "webpage_notify", -1);
+  if( g.thTrace ){
+    Th_Trace("[webpage_notify {%h}] => %h<br />\n", zName,
+             Th_ReturnCodeName(rc));
+  }
+  return rc;
+}
 
 /*
 ** The z[] input contains text mixed with TH1 scripts.
 ** The TH1 scripts are contained within <th1>...</th1>. 
 ** TH1 variables are $aaa or $<aaa>.  The first form of
@@ -932,5 +1067,34 @@
   db_open_config(0); /* Needed for global "tcl" setting. */
   blob_zero(&in);
   blob_read_from_file(&in, g.argv[2]);
   Th_Render(blob_str(&in));
 }
+
+/*
+** COMMAND: test-th-hook
+*/
+void test_th_hook(void){
+  int rc = TH_OK;
+  int nResult = 0;
+  char *zResult;
+  if( g.argc<5 ){
+    usage("TYPE NAME FLAGS");
+  }
+  if( fossil_stricmp(g.argv[2], "cmdhook")==0 ){
+    rc = Th_CommandHook(g.argv[3], (char)atoi(g.argv[4]));
+  }else if( fossil_stricmp(g.argv[2], "cmdnotify")==0 ){
+    rc = Th_CommandNotify(g.argv[3], (char)atoi(g.argv[4]));
+  }else if( fossil_stricmp(g.argv[2], "webhook")==0 ){
+    rc = Th_WebpageHook(g.argv[3], (char)atoi(g.argv[4]));
+  }else if( fossil_stricmp(g.argv[2], "webnotify")==0 ){
+    rc = Th_WebpageNotify(g.argv[3], (char)atoi(g.argv[4]));
+  }else{
+    fossil_fatal("Unknown TH1 hook %s\n", g.argv[2]);
+  }
+  zResult = (char*)Th_GetResult(g.interp, &nResult);
+  sendText("RESULT (", -1, 0);
+  sendText(Th_ReturnCodeName(rc), -1, 0);
+  sendText("): ", -1, 0);
+  sendText(zResult, nResult, 0);
+  sendText("\n", -1, 0);
+}