diff --git a/mininet/cli.py b/mininet/cli.py
index b432f7c94d93f537611f409f5daee166fdf74452..42a4d3d7f1d6724d916475d3f17d6766098802a7 100644
--- a/mininet/cli.py
+++ b/mininet/cli.py
@@ -55,7 +55,7 @@ def __init__( self, mininet, stdin=sys.stdin, script=None ):
         Cmd.__init__( self )
         info( '*** Starting CLI:\n' )
 
-        # Setup history if readline is available
+        # Set up history if readline is available
         try:
             import readline
         except ImportError:
@@ -77,7 +77,7 @@ def __init__( self, mininet, stdin=sys.stdin, script=None ):
                         node.sendInt()
                         node.monitor()
                 if self.isatty():
-                    quietRun( 'stty sane' )
+                    quietRun( 'stty echo sane intr "^C"' )
                 self.cmdloop()
                 break
             except KeyboardInterrupt:
@@ -352,8 +352,7 @@ def default( self, line ):
                      for arg in rest ]
             rest = ' '.join( rest )
             # Run cmd on node:
-            builtin = isShellBuiltin( first )
-            node.sendCmd( rest, printPid=( not builtin ) )
+            node.sendCmd( rest )
             self.waitForNode( node )
         else:
             error( '*** Unknown command: %s\n' % line )
@@ -361,7 +360,7 @@ def default( self, line ):
     # pylint: enable-msg=R0201
 
     def waitForNode( self, node ):
-        "Wait for a node to finish, and  print its output."
+        "Wait for a node to finish, and print its output."
         # Pollers
         nodePoller = poll()
         nodePoller.register( node.stdout )
@@ -379,7 +378,7 @@ def waitForNode( self, node ):
                 if False and self.inputFile:
                     key = self.inputFile.read( 1 )
                     if key is not '':
-                        node.write(key)
+                        node.write( key )
                     else:
                         self.inputFile = None
                 if isReadable( self.inPoller ):
@@ -391,8 +390,12 @@ def waitForNode( self, node ):
                 if not node.waiting:
                     break
             except KeyboardInterrupt:
+                # There is an at least one race condition here, since
+                # it's possible to interrupt ourselves after we've
+                # read data but before it has been printed.
                 node.sendInt()
 
+
 # Helper functions
 
 def isReadable( poller ):
diff --git a/mininet/node.py b/mininet/node.py
index 92ffcc19e4cd70a0b52a83ed9492bde576d2f79e..48424e70cf6faaee50943fcdf5123a7d06cf5381 100644
--- a/mininet/node.py
+++ b/mininet/node.py
@@ -50,6 +50,7 @@
 """
 
 import os
+import pty
 import re
 import signal
 import select
@@ -123,16 +124,22 @@ def startShell( self ):
             return
         # mnexec: (c)lose descriptors, (d)etach from tty,
         # (p)rint pid, and run in (n)amespace
-        opts = '-cdp'
+        opts = '-cd'
         if self.inNamespace:
             opts += 'n'
-        # bash -m: enable job control
+        # bash -m: enable job control, i: force interactive
         # -s: pass $* to shell, and make process easy to find in ps
-        cmd = [ 'mnexec', opts, 'bash', '-ms', 'mininet:' + self.name ]
-        self.shell = Popen( cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT,
-                            close_fds=True )
-        self.stdin = self.shell.stdin
-        self.stdout = self.shell.stdout
+        # prompt is set to sentinel chr( 127 )
+        os.environ[ 'PS1' ] = chr( 127 )
+        cmd = [ 'mnexec', opts, 'bash', '--norc', '-mis', 'mininet:' + self.name ]
+        # Spawn a shell subprocess in a pseudo-tty, to disable buffering
+        # in the subprocess and insulate it from signals (e.g. SIGINT)
+        # received by the parent
+        master, slave = pty.openpty()
+        self.shell = Popen( cmd, stdin=slave, stdout=slave, stderr=slave,
+                                  close_fds=False )
+        self.stdin = os.fdopen( master )
+        self.stdout = self.stdin
         self.pid = self.shell.pid
         self.pollOut = select.poll()
         self.pollOut.register( self.stdout )
@@ -145,7 +152,14 @@ def startShell( self ):
         self.lastCmd = None
         self.lastPid = None
         self.readbuf = ''
+        # Wait for prompt
+        while True:
+            data = self.read( 1024 )
+            if data[ -1 ] == chr( 127 ):
+                break
+            self.pollOut.poll()
         self.waiting = False
+        self.cmd( 'stty -echo' )
 
     def cleanup( self ):
         "Help python collect its garbage."
@@ -224,36 +238,29 @@ def sendCmd( self, *args, **kwargs ):
             # Replace empty commands with something harmless
             cmd = 'echo -n'
         self.lastCmd = cmd
-        printPid = printPid and not isShellBuiltin( cmd )
-        if len( cmd ) > 0 and cmd[ -1 ] == '&':
-            # print ^A{pid}\n{sentinel}
-            cmd += ' printf "\\001%d\n\\177" $! \n'
-        else:
-            # print sentinel
-            cmd += '; printf "\\177"'
-            if printPid and not isShellBuiltin( cmd ):
+        if printPid and not isShellBuiltin( cmd ):
+            if len( cmd ) > 0 and cmd[ -1 ] == '&':
+                # print ^A{pid}\n so monitor() can set lastPid
+                cmd += ' printf "\\001%d\n" $! \n'
+            else:
                 cmd = 'mnexec -p ' + cmd
         self.write( cmd + '\n' )
         self.lastPid = None
         self.waiting = True
 
-    def sendInt( self, sig=signal.SIGINT ):
+    def sendInt( self, intr=chr( 3 ) ):
         "Interrupt running command."
-        if self.lastPid:
-            try:
-                os.kill( self.lastPid, sig )
-            except OSError:
-                pass
+        self.write( intr )
 
-    def monitor( self, timeoutms=None ):
+    def monitor( self, timeoutms=None, findPid=True ):
         """Monitor and return the output of a command.
            Set self.waiting to False if command has completed.
            timeoutms: timeout in ms or None to wait indefinitely."""
         self.waitReadable( timeoutms )
         data = self.read( 1024 )
         # Look for PID
-        marker = chr( 1 ) + r'\d+\n'
-        if chr( 1 ) in data:
+        marker = chr( 1 ) + r'\d+\r\n'
+        if findPid and chr( 1 ) in data:
             markers = re.findall( marker, data )
             if markers:
                 self.lastPid = int( markers[ 0 ][ 1: ] )
@@ -322,7 +329,10 @@ def popen( self, *args, **kwargs ):
         # Shell requires a string, not a list!
         if defaults.get( 'shell', False ):
             cmd = ' '.join( cmd )
-        return Popen( cmd, **defaults )
+        old = signal.signal( signal.SIGINT, signal.SIG_IGN )
+        popen = Popen( cmd, **defaults )
+        signal.signal( signal.SIGINT, old )
+        return popen
 
     def pexec( self, *args, **kwargs ):
         """Execute a command using popen