diff --git a/src/inc/auth.inc.php b/src/inc/auth.inc.php
index 945dce7..ecfac3b 100644
--- a/src/inc/auth.inc.php
+++ b/src/inc/auth.inc.php
@@ -21,7 +21,7 @@
  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-function doAuthenticate() {
+function authenticate_local() {
     global $db;
     global $iface_expire;
     global $syslog_use, $syslog_ident, $syslog_facility;
@@ -118,6 +118,7 @@ function doAuthenticate() {
         auth();
     }
 }
+add_listener('hook_authenticate', 'authenticate_local');
 
 /*
  * Print the login form.
diff --git a/src/inc/plugin.inc.php b/src/inc/plugin.inc.php
new file mode 100644
index 0000000..a0f4e73
--- /dev/null
+++ b/src/inc/plugin.inc.php
@@ -0,0 +1,83 @@
+<?php
+/**
+ * Plugin API
+ *
+ * Dynamically add behavior by putting code in the plugins directory
+ * of the application root.  For the loader to include a plugin,
+ * there must be a file named:
+ *
+ *     plugins/[name]/[name].plugin.php
+ *
+ * Further imports, functions, or definitions can be done from
+ * that top-level script.
+ */
+
+$hook_listeners = array();
+
+/**
+ * Register function to be executed for the given hook
+ *
+ * @param string $hook
+ * @param mixed $function
+ */
+function add_listener($hook, $function) {
+    if (!$hook || !$function) {
+        trigger_error('add_listener requires both a hook name and a function', E_USER_ERROR);
+    }
+    global $hook_listeners;
+    $hook_listeners[$hook][] = $function;
+}
+
+/**
+ * Removes all listeners from the given hook
+ *
+ * @param string $hook
+ */
+function clear_listeners($hook) {
+    if (!$hook) {
+        trigger_error('clear_listeners requires a hook name', E_USER_ERROR);
+    }
+    global $hook_listeners;
+    $hook_listeners[$hook] = array();
+}
+
+/**
+ * Execute a hook, call registered listener functions
+ */
+function do_hook() {
+    global $hook_listeners;
+    $argc = func_num_args();
+    $argv = func_get_args();
+
+    if ($argc < 1) {
+        trigger_error('Missing argument in do_hook', E_USER_ERROR);
+    }
+
+    $hook_name = array_shift($argv);
+
+    if (!isset($hook_listeners[$hook_name])) {
+        return;
+    }
+
+    foreach ($hook_listeners[$hook_name] as $func) {
+        $response = call_user_func_array($func, $argv);
+    }
+}
+
+/**
+ * Look for plugins and perform imports
+ */
+function import_plugins() {
+    $plugins_dir = 'plugins';
+    $contents = scandir($plugins_dir);
+    foreach ($contents as $dir) {
+        if ($dir == '.' || $dir == '..') { continue; }
+        $plugin_file = $plugins_dir . DIRECTORY_SEPARATOR . $dir . DIRECTORY_SEPARATOR . $dir . '.plugin.php';
+        if (file_exists($plugin_file)) {
+            require_once $plugin_file;
+        }
+    }
+}
+import_plugins();
+
+?>
diff --git a/src/inc/toolkit.inc.php b/src/inc/toolkit.inc.php
index a6b5f0d..17f3482 100644
--- a/src/inc/toolkit.inc.php
+++ b/src/inc/toolkit.inc.php
@@ -134,6 +134,7 @@ if($dns_fancy) {
  * Includes  *
  *************/
 
+require_once("plugin.inc.php");
 require_once("i18n.inc.php");
 require_once("error.inc.php");
 require_once("auth.inc.php");
@@ -142,9 +143,10 @@ require_once("dns.inc.php");
 require_once("record.inc.php");
 require_once("templates.inc.php");
 
-$db = dbConnect();
-doAuthenticate();
+do_hook('hook_post_includes');
 
+$db = dbConnect();
+do_hook('hook_authenticate');
 
 /*************
  * Functions *
