diff --git a/doc/glue-code.md b/doc/glue-code.md
index 54404a7..a1498bc 100644
--- a/doc/glue-code.md
+++ b/doc/glue-code.md
@@ -25,17 +25,27 @@ look like:
         ld sp, hl
         im 1
         call aciaInit
-        call shellInit
+        xor	a
+        ld	de, BLOCKDEV_GETC
+        call	blkSel
+        call	stdioInit
+        call    shellInit
         ei
-        jp	shellLoop
+        jp      shellLoop
 
     #include "core.asm"
     .equ    ACIA_RAMSTART	RAMSTART
     #include "acia.asm"
-    .equ    SHELL_RAMSTART	ACIA_RAMEND
-    .define SHELL_GETC	call aciaGetC
-    .define SHELL_PUTC	call aciaPutC
-    .define SHELL_IO_GETC	call aciaGetC
+    .equ	BLOCKDEV_RAMSTART	ACIA_RAMEND
+    .equ	BLOCKDEV_COUNT		1
+    #include "blockdev.asm"
+    ; List of devices
+    .dw	aciaGetC, aciaPutC, 0, 0
+
+    .equ	STDIO_RAMSTART	BLOCKDEV_RAMEND
+    #include "stdio.asm"
+
+    .equ    SHELL_RAMSTART	STDIO_RAMEND
     .equ    SHELL_EXTRA_CMD_COUNT 0
     #include "shell.asm"
 
diff --git a/kernel/pgm.asm b/kernel/pgm.asm
new file mode 100644
index 0000000..ead37ac
--- /dev/null
+++ b/kernel/pgm.asm
@@ -0,0 +1,61 @@
+; pgm - execute programs loaded from filesystem
+;
+; Implements a shell hook that searches the filesystem for a file with the same
+; name as the cmd, loads that file in memory and executes it, sending the
+; program a pointer to *unparsed* arguments in HL.
+;
+; We expect the loaded program to return a status code in A. 0 means success,
+; non-zero means error. Programs should avoid having error code overlaps with
+; the shell so that we know where the error comes from.
+;
+; *** Defines ***
+; PGM_CODEADDR: Memory address where to place the code we load.
+
+; Routine suitable to plug into SHELL_CMDHOOK. HL points to the full cmdline.
+; We can mutate it because the shell doesn't do anything with it afterwards.
+pgmShellHook:
+	call	fsIsOn
+	jr	nz, .noFile
+	; first first space and replace it with zero so that we have something
+	; suitable for fsFindFN.
+	push	hl	; remember beginning
+	ld	a, ' '
+	call	findchar
+	jr	nz, .noarg	; if we have no arg, we want DE to point to the
+				; null char. Also, we have no replacement to
+				; make
+	; replace space with nullchar
+	xor	a
+	ld	(hl), a
+	inc	hl		; make HL point to the beginning of args
+.noarg:
+	ex	de, hl	; DE now points to the beginning of args or to \0 if
+			; no args
+	pop	hl	; HL points to cmdname, properly null-terminated
+	call	fsFindFN
+	jr	nz, .noFile
+	; We have a file! Load it and run it.
+	jp	pgmRun
+.noFile:
+	ld	a, SHELL_ERR_IO_ERROR
+	ret
+
+; Loads code in file that FS_PTR is currently pointing at and place it in
+; PGM_CODEADDR. Then, jump to PGM_CODEADDR.
+pgmRun:
+	call	fsIsValid
+	jr	nz, .ioError
+	ld	ix, FS_HANDLES
+	call	fsOpen
+	ld	hl, PGM_CODEADDR
+.loop:
+	call	fsGetC		; we use Z at end of loop
+	ld	(hl), a		; Z preserved
+	inc	hl		; Z preserved in 16-bit
+	jr	z, .loop
+	; ready to jump!
+	jp	PGM_CODEADDR
+
+.ioError:
+	ld	a, SHELL_ERR_IO_ERROR
+	ret
diff --git a/kernel/shell.asm b/kernel/shell.asm
index 2d49f63..3f6c28e 100644
--- a/kernel/shell.asm
+++ b/kernel/shell.asm
@@ -2,11 +2,17 @@
 ;
 ; Runs a shell over a block device interface.
 
-; Status: incomplete. As it is now, it spits a welcome prompt, wait for input
-; and compare the first 4 chars of the input with a command table and call the
-; appropriate routine if it's found, an error if it's not.
+; The shell spits a welcome prompt, wait for input and compare the first 4 chars
+; of the input with a command table and call the appropriate routine if it's
+; found, an error if it's not.
 ;
-; Commands, for now, are partially implemented.
+; To determine the correct routine to call we first go through cmds in
+; shellCmdTbl. This means that we first go through internal cmds, then cmds
+; "grafted" by glue code.
+;
+; If the command isn't found, SHELL_CMDHOOK is called, which should set A to
+; zero if it executes something. Otherwise, SHELL_ERR_UNKNOWN_CMD will be
+; returned.
 ;
 ; See constants below for error codes.
 ;
@@ -59,7 +65,10 @@
 ; for the null-termination every time we write to it. Simpler that way.
 .equ	SHELL_BUF	SHELL_CMD_ARGS+SHELL_CMD_ARGS_MAXSIZE
 
-.equ	SHELL_RAMEND	SHELL_BUF+SHELL_BUFSIZE
+; Pointer to a hook to call when a cmd name isn't found
+.equ	SHELL_CMDHOOK	SHELL_BUF+SHELL_BUFSIZE
+
+.equ	SHELL_RAMEND	SHELL_CMDHOOK+2
 
 ; *** CODE ***
 shellInit:
@@ -67,11 +76,12 @@ shellInit:
 	ld	(SHELL_MEM_PTR), a
 	ld	(SHELL_MEM_PTR+1), a
 	ld	(SHELL_BUF), a
+	ld	hl, noop
+	ld	(SHELL_CMDHOOK), hl
 
 	; print welcome
 	ld	hl, .welcome
-	call	printstr
-	ret
+	jp	printstr		; returns
 
 .welcome:
 	.db	"Collapse OS", ASCII_CR, ASCII_LF, "> ", 0
@@ -154,6 +164,12 @@ shellParse:
 
 	; exhausted loop? not found
 	ld	a, SHELL_ERR_UNKNOWN_CMD
+	; Before erroring out, let's try SHELL_HOOK.
+	ld	ix, (SHELL_CMDHOOK)
+	call	callIX
+	jr	z, .end		; oh, not an error!
+	; still an error. Might be different than SHELL_ERR_UNKNOWN_CMD though.
+	; maybe a routine was called, but errored out.
 	jr	.error
 
 .found:
@@ -171,7 +187,7 @@ shellParse:
 
 	; We're ready to parse args
 	call	shellParseArgs
-	cp	0
+	or	a		; cp 0
 	jr	nz, .parseerror
 
 	ld	hl, SHELL_CMD_ARGS
@@ -182,7 +198,7 @@ shellParse:
 	push	de \ pop ix
 	; Ready to roll!
 	call	callIX
-	cp	0
+	or	a		; cp 0
 	jr	nz, .error	; if A is non-zero, we have an error
 	jr	.end
 
diff --git a/recipes/rc2014/glue.asm b/recipes/rc2014/glue.asm
index 77fba48..7a55b04 100644
--- a/recipes/rc2014/glue.asm
+++ b/recipes/rc2014/glue.asm
@@ -26,7 +26,7 @@ jp	aciaInt
 #include "stdio.asm"
 
 .equ	SHELL_RAMSTART	STDIO_RAMEND
-.equ SHELL_EXTRA_CMD_COUNT 0
+.equ	SHELL_EXTRA_CMD_COUNT 0
 #include "shell.asm"
 
 init:
diff --git a/recipes/rc2014/sdcard/README.md b/recipes/rc2014/sdcard/README.md
index fd07d6c..38121d2 100644
--- a/recipes/rc2014/sdcard/README.md
+++ b/recipes/rc2014/sdcard/README.md
@@ -107,22 +107,21 @@ filesystem into the `sdcard.cfs` file. That can be mounted by Collapse OS!
 
 Then, you insert your SD card in your SPI relay and go:
 
-	Collapse OS
-	> mptr 9000
-	9000
-	> sdci
-	> bsel 1
-	> fson
-	> fls
-	helo
-	hello.txt
-	> fopn 0 helo
-	> load 10
-	> peek 10
-	210690C3030048656C6C6F210D0A0000
-	> call 00 0000
-	Hello!
-	>
+    Collapse OS
+    > sdci
+    > bsel 1
+    > fson
+    > fls
+    helo
+    hello.txt
+    > helo
+    Hello!
+    >
+
+The `helo` command is a bit magical and is due to the hook implemented in
+`pgm.asm`: when an unknown command is typed, it looks in the currently mounted
+filesystem for a file with the same name. If it finds it, it loads it in memory
+at a predefined place (in our case, `0x9000`) and executes it.
 
 Now let that sink in for a minute. You've just mounted a filesystem on a SD
 card, loaded a file from it in memory and executed that file, all that on a
diff --git a/recipes/rc2014/sdcard/glue.asm b/recipes/rc2014/sdcard/glue.asm
index 8d613ed..fb3b103 100644
--- a/recipes/rc2014/sdcard/glue.asm
+++ b/recipes/rc2014/sdcard/glue.asm
@@ -2,6 +2,7 @@
 ; The RAM module is selected on A15, so it has the range 0x8000-0xffff
 .equ	RAMSTART	0x8000
 .equ	RAMEND		0xffff
+.equ	PGM_CODEADDR	0x9000
 .equ	ACIA_CTL	0x80	; Control and status. RS off.
 .equ	ACIA_IO		0x81	; Transmit. RS on.
 
@@ -46,10 +47,12 @@ jp	aciaInt
 .dw	sdcInitializeCmd, blkBselCmd, blkSeekCmd
 .dw	fsOnCmd, flsCmd, fnewCmd, fdelCmd, fopnCmd
 
-.equ SDC_RAMSTART	SHELL_RAMEND
-.equ SDC_PORT_CSHIGH	6
-.equ SDC_PORT_CSLOW	5
-.equ SDC_PORT_SPI	4
+#include "pgm.asm"
+
+.equ	SDC_RAMSTART	SHELL_RAMEND
+.equ	SDC_PORT_CSHIGH	6
+.equ	SDC_PORT_CSLOW	5
+.equ	SDC_PORT_SPI	4
 #include "sdc.asm"
 
 init:
@@ -64,6 +67,8 @@ init:
 	call	blkSel
 	call	stdioInit
 	call	shellInit
+	ld	hl, pgmShellHook
+	ld	(SHELL_CMDHOOK), hl
 
 	ei
 	jp	shellLoop
diff --git a/recipes/rc2014/sdcard/helo.asm b/recipes/rc2014/sdcard/helo.asm
index 0c905b3..58b71c7 100644
--- a/recipes/rc2014/sdcard/helo.asm
+++ b/recipes/rc2014/sdcard/helo.asm
@@ -4,7 +4,9 @@
 .org	0x9000
 
 	ld	hl, sHello
-	jp	printstr	; return
+	call	printstr
+	xor	a		; success
+	ret
 
 sHello:
 	.db	"Hello!", 0x0d, 0x0a, 0
diff --git a/tools/emul/shell/shell_.asm b/tools/emul/shell/shell_.asm
index 1dad3d7..26d02c5 100644
--- a/tools/emul/shell/shell_.asm
+++ b/tools/emul/shell/shell_.asm
@@ -1,6 +1,7 @@
 ; named shell_.asm to avoid infinite include loop.
 .equ	RAMSTART	0x4000
-.equ	RAMEND		0x5000
+.equ	KERNEL_RAMEND	0x5000
+.equ	USERCODE	0x9000
 .equ	STDIO_PORT	0x00
 .equ	FS_DATA_PORT	0x01
 .equ	FS_SEEKL_PORT	0x02
@@ -9,6 +10,9 @@
 
 	jp	init
 
+; *** JUMP TABLE ***
+	jp	printstr
+
 #include "core.asm"
 #include "parse.asm"
 
@@ -36,10 +40,13 @@
 #include "shell.asm"
 .dw	blkBselCmd, blkSeekCmd, fsOnCmd, flsCmd, fnewCmd, fdelCmd, fopnCmd
 
+.equ	PGM_CODEADDR		USERCODE
+#include "pgm.asm"
+
 init:
 	di
 	; setup stack
-	ld	hl, RAMEND
+	ld	hl, KERNEL_RAMEND
 	ld	sp, hl
 	xor	a
 	ld	de, BLOCKDEV_GETC
@@ -54,6 +61,8 @@ init:
 	ld	de, BLOCKDEV_GETC
 	call	blkSel
 	call	shellInit
+	ld	hl, pgmShellHook
+	ld	(SHELL_CMDHOOK), hl
 	jp	shellLoop
 
 emulGetC: