diff --git a/code/_macros.dm b/code/_macros.dm
index 7e8d581b90..e8caac8345 100644
--- a/code/_macros.dm
+++ b/code/_macros.dm
@@ -7,6 +7,8 @@
#define isanimal(A) istype(A, /mob/living/simple_animal)
+#define isairlock(A) istype(A, /obj/machinery/door/airlock)
+
#define isbrain(A) istype(A, /mob/living/carbon/brain)
#define iscarbon(A) istype(A, /mob/living/carbon)
diff --git a/code/controllers/hooks-defs.dm b/code/controllers/hooks-defs.dm
index 001372b4e3..e330024475 100644
--- a/code/controllers/hooks-defs.dm
+++ b/code/controllers/hooks-defs.dm
@@ -1,87 +1,87 @@
-/**
- * Startup hook.
- * Called in world.dm when the server starts.
- */
-/hook/startup
-
-/**
- * Roundstart hook.
- * Called in gameticker.dm when a round starts.
- */
-/hook/roundstart
-
-/**
- * Roundend hook.
- * Called in gameticker.dm when a round ends.
- */
-/hook/roundend
-
-/**
- * Death hook.
- * Called in death.dm when someone dies.
- * Parameters: var/mob/living/carbon/human, var/gibbed
- */
-/hook/death
-
-/**
- * Cloning hook.
- * Called in cloning.dm when someone is brought back by the wonders of modern science.
- * Parameters: var/mob/living/carbon/human
- */
-/hook/clone
-
-/**
- * Debrained hook.
- * Called in brain_item.dm when someone gets debrained.
- * Parameters: var/obj/item/organ/brain
- */
-/hook/debrain
-
-/**
- * Borged hook.
- * Called in robot_parts.dm when someone gets turned into a cyborg.
- * Parameters: var/mob/living/silicon/robot
- */
-/hook/borgify
-
-/**
- * Podman hook.
- * Called in podmen.dm when someone is brought back as a Diona.
- * Parameters: var/mob/living/carbon/alien/diona
- */
-/hook/harvest_podman
-
-/**
- * Payroll revoked hook.
- * Called in Accounts_DB.dm when someone's payroll is stolen at the Accounts terminal.
- * Parameters: var/datum/money_account
- */
-/hook/revoke_payroll
-
-/**
- * Account suspension hook.
- * Called in Accounts_DB.dm when someone's account is suspended or unsuspended at the Accounts terminal.
- * Parameters: var/datum/money_account
- */
-/hook/change_account_status
-
-/**
- * Employee reassignment hook.
- * Called in card.dm when someone's card is reassigned at the HoP's desk.
- * Parameters: var/obj/item/weapon/card/id
- */
-/hook/reassign_employee
-
-/**
- * Employee terminated hook.
- * Called in card.dm when someone's card is terminated at the HoP's desk.
- * Parameters: var/obj/item/weapon/card/id
- */
-/hook/terminate_employee
-
-/**
- * Crate sold hook.
- * Called in supplyshuttle.dm when a crate is sold on the shuttle.
- * Parameters: var/obj/structure/closet/crate/sold, var/area/shuttle
- */
-/hook/sell_crate
+/**
+ * Startup hook.
+ * Called in world.dm when the server starts.
+ */
+/hook/startup
+
+/**
+ * Roundstart hook.
+ * Called in gameticker.dm when a round starts.
+ */
+/hook/roundstart
+
+/**
+ * Roundend hook.
+ * Called in gameticker.dm when a round ends.
+ */
+/hook/roundend
+
+/**
+ * Death hook.
+ * Called in death.dm when someone dies.
+ * Parameters: var/mob/living/carbon/human, var/gibbed
+ */
+/hook/death
+
+/**
+ * Cloning hook.
+ * Called in cloning.dm when someone is brought back by the wonders of modern science.
+ * Parameters: var/mob/living/carbon/human
+ */
+/hook/clone
+
+/**
+ * Debrained hook.
+ * Called in brain_item.dm when someone gets debrained.
+ * Parameters: var/obj/item/organ/brain
+ */
+/hook/debrain
+
+/**
+ * Borged hook.
+ * Called in robot_parts.dm when someone gets turned into a cyborg.
+ * Parameters: var/mob/living/silicon/robot
+ */
+/hook/borgify
+
+/**
+ * Podman hook.
+ * Called in podmen.dm when someone is brought back as a Diona.
+ * Parameters: var/mob/living/carbon/alien/diona
+ */
+/hook/harvest_podman
+
+/**
+ * Payroll revoked hook.
+ * Called in Accounts_DB.dm when someone's payroll is stolen at the Accounts terminal.
+ * Parameters: var/datum/money_account
+ */
+/hook/revoke_payroll
+
+/**
+ * Account suspension hook.
+ * Called in Accounts_DB.dm when someone's account is suspended or unsuspended at the Accounts terminal.
+ * Parameters: var/datum/money_account
+ */
+/hook/change_account_status
+
+/**
+ * Employee reassignment hook.
+ * Called in card.dm when someone's card is reassigned at the HoP's desk.
+ * Parameters: var/obj/item/weapon/card/id
+ */
+/hook/reassign_employee
+
+/**
+ * Employee terminated hook.
+ * Called in card.dm when someone's card is terminated at the HoP's desk.
+ * Parameters: var/obj/item/weapon/card/id
+ */
+/hook/terminate_employee
+
+/**
+ * Crate sold hook.
+ * Called in supplyshuttle.dm when a crate is sold on the shuttle.
+ * Parameters: var/obj/structure/closet/crate/sold, var/area/shuttle
+ */
+/hook/sell_crate
diff --git a/code/controllers/observer_listener/atom/observer.dm b/code/controllers/observer_listener/atom/observer.dm
new file mode 100644
index 0000000000..da38580414
--- /dev/null
+++ b/code/controllers/observer_listener/atom/observer.dm
@@ -0,0 +1,31 @@
+#define OBSERVER_EVENT_DESTROY "OnDestroy"
+
+/atom
+ var/list/observer_events
+
+/atom/Destroy()
+ var/list/destroy_listeners = get_listener_list_from_event(OBSERVER_EVENT_DESTROY)
+ if(destroy_listeners)
+ for(var/destroy_listener in destroy_listeners)
+ call(destroy_listener, destroy_listeners[destroy_listener])(src)
+
+ for(var/list/listeners in observer_events)
+ listeners.Cut()
+
+ return ..()
+
+/atom/proc/register(var/event, var/procOwner, var/proc_call)
+ var/list/listeners = get_listener_list_from_event(event)
+ listeners[procOwner] = proc_call
+
+/atom/proc/unregister(var/event, var/procOwner)
+ var/list/listeners = get_listener_list_from_event(event)
+ listeners -= procOwner
+
+/atom/proc/get_listener_list_from_event(var/observer_event)
+ if(!observer_events) observer_events = list()
+ var/list/listeners = observer_events[observer_event]
+ if(!listeners)
+ listeners = list()
+ observer_events[observer_event] = listeners
+ return listeners
diff --git a/code/controllers/observer_listener/datum/observer.dm b/code/controllers/observer_listener/datum/observer.dm
new file mode 100644
index 0000000000..61f6fbf180
--- /dev/null
+++ b/code/controllers/observer_listener/datum/observer.dm
@@ -0,0 +1,28 @@
+/*
+#define OBSERVER_EVENT_DESTROY "OnDestroy"
+
+/datum
+ var/list/observer_events
+
+/datum/Destroy()
+ for(var/list/listeners in observer_events)
+ listeners.Cut()
+
+ return ..()
+
+/datum/proc/register(var/event, var/procOwner, var/proc_call)
+ var/list/listeners = get_listener_list_from_event(event)
+ listeners[procOwner] = proc_call
+
+/datum/proc/unregister(var/event, var/procOwner)
+ var/list/listeners = get_listener_list_from_event(event)
+ listeners -= procOwner
+
+/datum/proc/get_listener_list_from_event(var/observer_event)
+ if(!observer_events) observer_events = list()
+ var/list/listeners = observer_events[observer_event]
+ if(!listeners)
+ listeners = list()
+ observer_events[observer_event] = listeners
+ return listeners
+*/
diff --git a/code/game/objects/items/devices/hacktool.dm b/code/game/objects/items/devices/hacktool.dm
new file mode 100644
index 0000000000..0cf4af6c74
--- /dev/null
+++ b/code/game/objects/items/devices/hacktool.dm
@@ -0,0 +1,100 @@
+/obj/item/device/multitool/hacktool
+ var/is_hacking = 0
+ var/max_known_targets
+
+ var/in_hack_mode = 0
+ var/list/known_targets
+ var/list/supported_types
+ var/datum/topic_state/default/must_hack/hack_state
+
+/obj/item/device/multitool/hacktool/New()
+ ..()
+ known_targets = list()
+ max_known_targets = 5 + rand(1,3)
+ supported_types = list(/obj/machinery/door/airlock)
+ hack_state = new(src)
+
+/obj/item/device/multitool/hacktool/Destroy()
+ for(var/T in known_targets)
+ var/atom/target = T
+ target.unregister(OBSERVER_EVENT_DESTROY, src)
+ known_targets.Cut()
+ qdel(hack_state)
+ hack_state = null
+ return ..()
+
+/obj/item/device/multitool/hacktool/attackby(var/obj/W, var/mob/user)
+ if(isscrewdriver(W))
+ in_hack_mode = !in_hack_mode
+ playsound(src.loc, 'sound/items/Screwdriver.ogg', 50, 1)
+ else
+ ..()
+
+/obj/item/device/multitool/hacktool/resolve_attackby(atom/A, mob/user)
+ sanity_check()
+
+ if(!in_hack_mode)
+ return ..()
+
+ if(!attempt_hack(user, A))
+ return 0
+
+ A.ui_interact(user, state = hack_state)
+ return 1
+
+/obj/item/device/multitool/hacktool/proc/attempt_hack(var/mob/user, var/atom/target)
+ if(is_hacking)
+ user << "You are already hacking!"
+ return 0
+ if(!is_type_in_list(target, supported_types))
+ user << "\icon[src] Unable to hack this target!"
+ return 0
+ var/found = known_targets.Find(target)
+ if(found)
+ known_targets.Swap(1, found) // Move the last hacked item first
+ return 1
+
+ user << "You begin hacking \the [target]..."
+ is_hacking = 1
+ // On average hackin takes ~30 seconds. Fairly small random span to avoid people simply aborting and trying again
+ var/hack_result = do_after(user, (20 SECONDS + rand(0, 10 SECONDS) + rand(0, 10 SECONDS)))
+ is_hacking = 0
+
+ if(hack_result && in_hack_mode)
+ user << "Your hacking attempt was succesful!"
+ playsound(src.loc, 'sound/piano/A#6.ogg', 75)
+ else
+ user << "Your hacking attempt failed!"
+ return 0
+
+ known_targets.Insert(1, target) // Insert the newly hacked target first,
+ target.register(OBSERVER_EVENT_DESTROY, src, /obj/item/device/multitool/hacktool/proc/on_target_destroy)
+ return 1
+
+/obj/item/device/multitool/hacktool/proc/sanity_check()
+ if(max_known_targets < 1) max_known_targets = 1
+ // Cut away the oldest items if the capacity has been reached
+ if(known_targets.len > max_known_targets)
+ for(var/i = (max_known_targets + 1) to known_targets.len)
+ var/atom/A = known_targets[i]
+ A.unregister(OBSERVER_EVENT_DESTROY, src)
+ known_targets.Cut(max_known_targets + 1)
+
+/obj/item/device/multitool/hacktool/proc/on_target_destroy(var/target)
+ known_targets -= target
+
+/datum/topic_state/default/must_hack
+ var/obj/item/device/multitool/hacktool/hacktool
+
+/datum/topic_state/default/must_hack/New(var/hacktool)
+ src.hacktool = hacktool
+ ..()
+
+/datum/topic_state/default/must_hack/Destroy()
+ hacktool = null
+ return ..()
+
+/datum/topic_state/default/must_hack/can_use_topic(var/src_object, var/mob/user)
+ if(!hacktool || !hacktool.in_hack_mode || !(src_object in hacktool.known_targets))
+ return STATUS_CLOSE
+ return ..()
diff --git a/code/game/objects/items/devices/multitool.dm b/code/game/objects/items/devices/multitool.dm
index 867e64f446..e7cb6f3475 100644
--- a/code/game/objects/items/devices/multitool.dm
+++ b/code/game/objects/items/devices/multitool.dm
@@ -20,4 +20,4 @@
origin_tech = list(TECH_MAGNET = 1, TECH_ENGINEERING = 1)
var/obj/machinery/telecomms/buffer // simple machine buffer for device linkage
- var/obj/machinery/clonepod/connecting //same for cryopod linkage
\ No newline at end of file
+ var/obj/machinery/clonepod/connecting //same for cryopod linkage
diff --git a/code/game/objects/items/devices/uplink_items.dm b/code/game/objects/items/devices/uplink_items.dm
index bc9666990b..b0ee3689a1 100644
--- a/code/game/objects/items/devices/uplink_items.dm
+++ b/code/game/objects/items/devices/uplink_items.dm
@@ -411,6 +411,14 @@ datum/uplink_item/dd_SortValue()
item_cost = 3
path = /obj/item/weapon/card/emag
+/datum/uplink_item/item/tools/hacking_tool
+ name = "Door Hacking Tool"
+ item_cost = 2
+ path = /obj/item/device/multitool/hacktool
+ desc = "Appears and functions as a standard multitool until the mode is toggled by applying a screwdriver appropriately. \
+ When in hacking mode this device will grant full access to any standard airlock within 20 to 40 seconds. \
+ This device will also be able to immediately access the last 6 to 8 hacked airlocks."
+
/datum/uplink_item/item/tools/clerical
name = "Morphic Clerical Kit"
item_cost = 3
diff --git a/polaris.dme b/polaris.dme
index 588cc73c5c..fa49981699 100644
--- a/polaris.dme
+++ b/polaris.dme
@@ -127,6 +127,8 @@
#include "code\controllers\subsystems.dm"
#include "code\controllers\verbs.dm"
#include "code\controllers\voting.dm"
+#include "code\controllers\observer_listener\atom\observer.dm"
+#include "code\controllers\observer_listener\datum\observer.dm"
#include "code\controllers\Processes\air.dm"
#include "code\controllers\Processes\alarm.dm"
#include "code\controllers\Processes\chemistry.dm"
@@ -612,6 +614,7 @@
#include "code\game\objects\items\devices\flash.dm"
#include "code\game\objects\items\devices\flashlight.dm"
#include "code\game\objects\items\devices\floor_painter.dm"
+#include "code\game\objects\items\devices\hacktool.dm"
#include "code\game\objects\items\devices\lightreplacer.dm"
#include "code\game\objects\items\devices\locker_painter.dm"
#include "code\game\objects\items\devices\megaphone.dm"