diff --git a/plugins/help b/plugins/help
index 38e0d45..f3abed2 100644
--- a/plugins/help
+++ b/plugins/help
@@ -20,6 +20,7 @@ sub command :Tab(help,wtf,omgwtfbbq) ($self, $cart, $command, @) {
     # On the other hand, busybox(1) has a "more" applet that gives the user
     # clear instructions and seems mostly harmless too.
     my $pipe;
+    my $oldhandle = select;
     if (open $pipe, "|-", "busybox", "more") {
         select $pipe;
     }
@@ -49,7 +50,7 @@ ${bold}Advanced usage:${off} pass space separated arguments to parameters
 Complete each transaction with ${underline}account${off} (i.e. enter your name).
 END
 
-    select STDOUT;
+    select $oldhandle;
     close $pipe;
 
     return ACCEPT;
diff --git a/plugins/json b/plugins/json
new file mode 100644
index 0000000..2f3ad5e
--- /dev/null
+++ b/plugins/json
@@ -0,0 +1,88 @@
+#!perl
+
+=head1 CAVEATS
+
+This module requires the Perl module "JSON" to be installed.
+
+Note that cent amounts are emitted as strings, not floats. This is on purpose.
+They are, however, in a format that is easy to parse and convert (e.g.
+JavaScript "parseFloat").
+
+Note that things may be happening that don't have any JSON output.
+
+Note that if plugins explicitly print to STDOUT, that will break the JSON
+output. Regular print (without specified filehandle) will be suppressed.
+
+Note that one command line may result in several separate transactions.
+
+Note that this plugin will always be highly experimental; re-evaluate your
+assumptions when upgrading. :)
+
+This plugin is intended to be used together with "revbank -c 'command line'",
+but you could try to use it interactively; if you do, please let me know about
+your use case.
+
+Set the environment variable REVBANK_JSON to either "array" or "lines" (see
+jsonlines.org).
+
+=cut
+
+use JSON;
+my $json = JSON->new->utf8->convert_blessed->canonical;
+
+BEGIN {
+    if ($ENV{REVBANK_JSON} and $ENV{REVBANK_JSON} =~ /^(?:array|lines)$/) {
+        my $array = $ENV{REVBANK_JSON} eq "array";
+
+        # Suppress normal print output
+        open my $null, ">", "/dev/null";
+        select $null;
+
+        print STDOUT "[\n" if $array;
+
+        my $count = 0;
+        *_log = sub($hash) {
+            # JSON does not allow trailing commas, argh
+            print STDOUT ",\n" if $array and $count++;
+            print STDOUT $json->encode($hash);
+            print STDOUT "\n" if not $array;
+        };
+
+        END { print STDOUT "\n]\n" if $array }
+
+        # Monkey patch
+        *RevBank::Amount::TO_JSON = sub($self, @) {
+            $self->string("+");
+        };
+    } else {
+        *_log = sub { };
+    }
+}
+
+
+sub hook_abort(@) {
+    _log({ _ => "ABORT" });
+}
+
+sub hook_reject($class, $plugin, $reason, $abort, @) {
+    _log({ _ => "REJECT", plugin => $plugin, reason => $reason, abort => $abort });
+}
+
+sub hook_retry($class, $plugin, $reason, $abort, @) {
+    _log({ _ => "RETRY", plugin => $plugin, reason => $reason, abort => $abort });
+}
+
+sub hook_user_created($class, $username, @) {
+    _log({ _ => "NEWUSER", account => $username });
+}
+
+# NB: stringify transaction_id because future ids might not be numeric.
+
+sub hook_user_balance($class, $user, $old, $delta, $new, $transaction_id, @) {
+    _log({ _ => "BALANCE", account => $user, old => $old, delta => $delta, new => $new, transaction_id => "$transaction_id" });
+}
+
+sub hook_checkout($class, $cart, $username, $transaction_id, @) {
+    _log({ _ => "CHECKOUT", account => $username, transaction_id => "$transaction_id" });
+}
+