diff --git a/core/src/cli/command-line.ts b/core/src/cli/command-line.ts
index 735e47cffd..ab88964912 100644
--- a/core/src/cli/command-line.ts
+++ b/core/src/cli/command-line.ts
@@ -719,6 +719,7 @@ ${chalk.white.underline("Keys:")}
         this.flashError(getCmdFailMsg(name))
       })
       .finally(() => {
+        this.garden.events.clearKey(this.garden.sessionId)
         delete this.runningCommands[id]
         this.renderStatus()
       })
diff --git a/core/src/events.ts b/core/src/events.ts
index 683e33e278..3d50507092 100644
--- a/core/src/events.ts
+++ b/core/src/events.ts
@@ -30,12 +30,17 @@ export type GardenEventListener<T extends EventName> = (payload: Events[T]) => v
  * See below for the event interfaces.
  */
 export class EventBus extends EventEmitter2 {
-  constructor() {
+  private keyIndex: {
+    [key: string]: { [eventName: string]: ((payload: any) => void)[] }
+  }
+
+  constructor(name?: string) {
     super({
       wildcard: false,
       newListener: false,
       maxListeners: 5000, // we may need to adjust this
     })
+    this.keyIndex = {}
   }
 
   emit<T extends EventName>(name: T, payload: Events[T]) {
@@ -46,6 +51,53 @@ export class EventBus extends EventEmitter2 {
     return super.on(name, listener)
   }
 
+  /**
+   * Registers the listener under the provided key for easy cleanup via `offKey`. This is useful e.g. for the
+   * plugin event broker, which is instantiated in several places and where there isn't a single obvious place to
+   * remove listeners from all instances generated in a single command run.
+   */
+  onKey<T extends EventName>(name: T, listener: (payload: Events[T]) => void, key: string) {
+    if (!this.keyIndex[key]) {
+      this.keyIndex[key] = {}
+    }
+    if (!this.keyIndex[key][name]) {
+      this.keyIndex[key][name] = []
+    }
+    this.keyIndex[key][name].push(listener)
+    return super.on(name, listener)
+  }
+
+  /**
+   * Removes all event listeners for the event `name` that were registered under `key` (via `onKey`).
+   */
+  offKey<T extends EventName>(name: T, key: string) {
+    if (!this.keyIndex[key]) {
+      return
+    }
+    if (!this.keyIndex[key][name]) {
+      return
+    }
+    for (const listener of this.keyIndex[key][name]) {
+      this.removeListener(name, listener)
+    }
+    delete this.keyIndex[key][name]
+  }
+
+  /**
+   * Removes all event listeners that were registered under `key` (via `onKey`).
+   */
+  clearKey(key: string) {
+    if (!this.keyIndex[key]) {
+      return
+    }
+    for (const name of Object.keys(this.keyIndex[key])) {
+      for (const listener of this.keyIndex[key][name]) {
+        this.removeListener(name, listener)
+      }
+    }
+    delete this.keyIndex[key]
+  }
+
   onAny(listener: <T extends EventName>(name: T, payload: Events[T]) => void) {
     return super.onAny(<any>listener)
   }
@@ -260,8 +312,8 @@ export type EventName = keyof Events
 
 // Note: Does not include logger events.
 export const pipedEventNames: EventName[] = [
-  "_exit",
-  "_restart",
+  // "_exit",
+  // "_restart",
   "_test",
   "_workflowRunRegistered",
   "sessionCompleted",
diff --git a/core/src/plugin-context.ts b/core/src/plugin-context.ts
index bc5faa314c..59138a1832 100644
--- a/core/src/plugin-context.ts
+++ b/core/src/plugin-context.ts
@@ -131,6 +131,8 @@ export class PluginEventBroker extends EventEmitter<PluginEvents, PluginEventTyp
   private done: boolean
   private failed: boolean
   private error: Error | undefined
+  private garden: Garden
+  private abortHandler: () => void
 
   constructor(garden: Garden) {
     super()
@@ -138,14 +140,13 @@ export class PluginEventBroker extends EventEmitter<PluginEvents, PluginEventTyp
     this.aborted = false
     this.done = false
     this.failed = false
+    this.garden = garden
+    this.abortHandler = () => this.emit("abort")
 
+    // console.trace()
     // Always respond to exit and restart events
-    garden.events.on("_exit", () => {
-      this.emit("abort")
-    })
-    garden.events.on("_restart", () => {
-      this.emit("abort")
-    })
+    this.garden.events.onKey("_exit", this.abortHandler, garden.sessionId)
+    this.garden.events.onKey("_restart", this.abortHandler, garden.sessionId)
 
     this.on("abort", () => {
       this.aborted = true
diff --git a/core/src/server/server.ts b/core/src/server/server.ts
index c41a2105fe..7cd7cf6ded 100644
--- a/core/src/server/server.ts
+++ b/core/src/server/server.ts
@@ -270,6 +270,7 @@ export class GardenServer extends EventEmitter {
         args,
         opts,
       })
+      this.garden.events.clearKey(this.garden.sessionId)
 
       if (result.errors?.length) {
         throw result.errors[0]
diff --git a/core/test/unit/src/events.ts b/core/test/unit/src/events.ts
index 4faeda317b..7264e626a9 100644
--- a/core/test/unit/src/events.ts
+++ b/core/test/unit/src/events.ts
@@ -6,6 +6,7 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  */
 
+import { range } from "lodash"
 import { EventBus } from "../../../src/events"
 import { expect } from "chai"
 
@@ -45,4 +46,61 @@ describe("EventBus", () => {
       events.emit("_test", "bar")
     })
   })
+
+  describe("onKey", () => {
+    it("should add a listener under the specified key", (done) => {
+      const key = "gandalf"
+      events.onKey("_test", (payload) => {
+        expect(payload).to.equal("foo")
+        expect(events["keyIndex"][key]["_test"].length).to.eql(1)
+        done()
+      }, key)
+      events.emit("_test", "foo")
+    })
+  })
+
+  describe("offKey", () => {
+    it("should remove all listeners with the specified key and name", () => {
+      const key = "gandalf"
+      const otherKey = "blob"
+      for (const _i of range(3)) {
+        events.onKey("_test", () => {}, key)
+        events.onKey("_restart", () => {}, key)
+
+        events.onKey("_test", () => {}, otherKey)
+        events.onKey("_restart", () => {}, otherKey)
+      }
+      expect(events.listenerCount()).to.eql(12)
+      events.offKey("_test", key)
+      expect(events.listenerCount()).to.eql(9)
+      expect(events["keyIndex"][key]["_test"]).to.be.undefined
+
+      // We expect the index for other key + name combinations to be the same.
+      expect(events["keyIndex"][key]["_restart"].length).to.eql(3)
+      expect(events["keyIndex"][otherKey]["_test"].length).to.eql(3)
+      expect(events["keyIndex"][otherKey]["_restart"].length).to.eql(3)
+    })
+  })
+
+  describe("clearKey", () => {
+    it("should remove all listeners with the specified key", () => {
+      const key = "gandalf"
+      const otherKey = "blob"
+      for (const _i of range(3)) {
+        events.onKey("_test", () => {}, key)
+        events.onKey("_restart", () => {}, key)
+
+        events.onKey("_test", () => {}, otherKey)
+        events.onKey("_restart", () => {}, otherKey)
+      }
+      expect(events.listenerCount()).to.eql(12)
+      events.clearKey(key)
+      expect(events.listenerCount()).to.eql(6)
+      expect(events["keyIndex"][key]).to.be.undefined
+
+      // We expect the index for the other key to be the same.
+      expect(events["keyIndex"][otherKey]["_test"].length).to.eql(3)
+      expect(events["keyIndex"][otherKey]["_restart"].length).to.eql(3)
+    })
+  })
 })