diff --git a/Cargo.lock b/Cargo.lock
index b47ca5b89e4b5290c2df01beec1b78620e63ca9f..eabcf14a84ca727623983621240ff11966700f7f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -345,6 +345,7 @@ dependencies = [
  "async-trait",
  "door-sshproto",
  "futures 0.4.0-alpha.0",
+ "heapless",
  "libc",
  "log",
  "moro",
diff --git a/async/Cargo.toml b/async/Cargo.toml
index 12863e490fafc8ba41a94c47b70285a5103e1f32..eb07034dfd65766a7a2797f7330c55394f936430 100644
--- a/async/Cargo.toml
+++ b/async/Cargo.toml
@@ -29,6 +29,8 @@ moro = "0.4"
 libc = "0.2"
 nix = "0.24"
 
+heapless = "0.7.10"
+
 # TODO
 pretty-hex = "0.3"
 snafu = { version = "0.7", default-features = true }
diff --git a/async/examples/con1.rs b/async/examples/con1.rs
index a7116e2e015f4f98d2814db2cd479fcf1fe85e73..3e97b7ab66333f9fe4b07ac70b921ee33064006e 100644
--- a/async/examples/con1.rs
+++ b/async/examples/con1.rs
@@ -11,7 +11,7 @@ use tokio::net::TcpStream;
 use std::{net::Ipv6Addr, io::Read};
 
 use door_sshproto::*;
-use door_async::SSHClient;
+use door_async::{SSHClient, raw_pty};
 
 use simplelog::*;
 
@@ -31,6 +31,10 @@ struct Args {
     /// a path to id_ed25519 or similar
     identityfile: Vec<String>,
 
+    #[argh(option)]
+    /// log to a file
+    tracefile: Option<String>,
+
     #[argh(option, short='l')]
     /// username
     username: Option<String>,
@@ -88,7 +92,7 @@ fn main() -> Result<()> {
         })
 }
 
-fn setup_log(args: &Args) {
+fn setup_log(args: &Args) -> Result<()> {
     let mut conf = simplelog::ConfigBuilder::new();
     let conf = conf
     .add_filter_allow_str("door")
@@ -108,11 +112,17 @@ fn setup_log(args: &Args) {
         LevelFilter::Warn
     };
 
-    CombinedLogger::init(
-    vec![
-        TermLogger::new(level, conf, TerminalMode::Mixed, ColorChoice::Auto),
-    ]
-    ).unwrap();
+    let mut logs: Vec<Box<dyn SharedLogger>> = vec![
+        TermLogger::new(level, conf.clone(), TerminalMode::Mixed, ColorChoice::Auto),
+    ];
+
+    if let Some(tf) = args.tracefile.as_ref() {
+        let w = std::fs::File::create(tf).with_context(|| format!("Error opening {tf}"))?;
+        logs.push(WriteLogger::new(LevelFilter::Trace, conf, w));
+    }
+
+    CombinedLogger::init(logs).unwrap();
+    Ok(())
 }
 
 fn read_key(p: &str) -> Result<SignKey> {
@@ -133,18 +143,16 @@ async fn run(args: &Args) -> Result<()> {
     // TODO: better lifetime rather than leaking
     let work = Box::leak(Box::new(work));
 
-    let mut sess = door_async::CmdlineClient::new(args.username.as_ref().unwrap());
+    let mut cli = door_async::CmdlineClient::new(args.username.as_ref().unwrap());
     for i in &args.identityfile {
-        sess.add_authkey(read_key(&i).with_context(|| format!("loading key {i}"))?);
+        cli.add_authkey(read_key(&i).with_context(|| format!("loading key {i}"))?);
     }
 
-    let mut door = SSHClient::new(work.as_mut_slice(), Box::new(sess))?;
-
+    let mut door = SSHClient::new(work.as_mut_slice(), Box::new(cli))?;
     let mut s = door.socket();
-    let netloop = tokio::io::copy_bidirectional(&mut stream, &mut s);
 
     moro::async_scope!(|scope| {
-        scope.spawn(netloop);
+        scope.spawn(tokio::io::copy_bidirectional(&mut stream, &mut s));
 
         scope.spawn(async {
             loop {
@@ -159,25 +167,40 @@ async fn run(args: &Args) -> Result<()> {
 
                 match ev {
                     Some(Event::Authenticated) => {
+                        let mut raw_pty_guard = None;
                         info!("Opening a new session channel");
-                        let cmd = if args.cmd.is_empty() {
-                            None
+                        let (cmd, pty) = if args.cmd.is_empty() {
+                            (None, true)
+                        } else {
+                            (Some(args.cmd.join(" ")), false)
+                        };
+                        let (mut io, mut err) = if pty {
+                            raw_pty_guard = Some(raw_pty()?);
+                            let io = door.open_client_session_pty(cmd.as_deref()).await
+                                .context("Opening session")?;
+                            (io, None)
                         } else {
-                            Some(args.cmd.join(" "))
+                            let (io, err) = door.open_client_session_nopty(cmd.as_deref()).await
+                                .context("Opening session")?;
+                            (io, Some(err))
+                        };
+                        let mut i = door_async::stdin()?;
+                        let mut o = door_async::stdout()?;
+                        let mut e = if err.is_some() {
+                            Some(door_async::stderr()?)
+                        } else {
+                            None
                         };
-                        let r = door.open_client_session_nopty(cmd.as_deref()).await
-                            .context("Opening session")?;
-                        let (mut io, mut err) = r;
+                        let mut io2 = io.clone();
                         scope.spawn(async move {
-                            let mut i = door_async::stdin()?;
-                            let mut o = door_async::stdout()?;
-                            let mut e = door_async::stderr()?;
-                            let mut io2 = io.clone();
                             moro::async_scope!(|scope| {
                                 scope.spawn(tokio::io::copy(&mut io, &mut o));
                                 scope.spawn(tokio::io::copy(&mut i, &mut io2));
-                                scope.spawn(tokio::io::copy(&mut err, &mut e));
+                                if let Some(ref mut err) = err {
+                                    scope.spawn(tokio::io::copy(err, e.as_mut().unwrap()));
+                                }
                             }).await;
+                            drop(raw_pty_guard);
                             Ok::<_, anyhow::Error>(())
                         });
                         // TODO: handle channel completion
diff --git a/async/src/async_door.rs b/async/src/async_door.rs
index 4432fe117042a6ddb039e9c3ee97b812243b4c40..13ae34204fe448e5105ac26c38b03260defe09ca 100644
--- a/async/src/async_door.rs
+++ b/async/src/async_door.rs
@@ -253,7 +253,9 @@ impl<'a> AsyncWrite for AsyncDoorSocket<'a> {
         self: Pin<&mut Self>,
         _cx: &mut Context<'_>,
     ) -> Poll<Result<(), IoError>> {
-        todo!("poll_close")
+        // TODO
+        error!("connection closed");
+        Poll::Ready(Ok(()))
     }
 }
 
diff --git a/async/src/client.rs b/async/src/client.rs
index 247dd355dc7dd984ded31def10374e88daacee88..acc2f8c94bbecde0500a120ede8986d4a2ee01cf 100644
--- a/async/src/client.rs
+++ b/async/src/client.rs
@@ -22,6 +22,7 @@ use crate::async_door::*;
 use door_sshproto as door;
 use door::{Behaviour, AsyncCliBehaviour, Runner, Result};
 use door::sshnames::SSH_EXTENDED_DATA_STDERR;
+use door::config::*;
 
 pub struct SSHClient<'a> {
     door: AsyncDoor<'a>,
@@ -52,7 +53,7 @@ impl<'a> SSHClient<'a> {
     pub async fn open_client_session_nopty(&mut self, exec: Option<&str>)
     -> Result<(ChanInOut<'a>, ChanExtIn<'a>)> {
         let chan = self.door.with_runner(|runner| {
-            runner.open_client_session(exec, false)
+            runner.open_client_session(exec, None)
         }).await?;
 
         let cstd = ChanInOut::new(chan, &self.door);
@@ -62,8 +63,12 @@ impl<'a> SSHClient<'a> {
 
     pub async fn open_client_session_pty(&mut self, exec: Option<&str>)
     -> Result<ChanInOut<'a>> {
+
+        // XXX error handling
+        let pty = pty::current_pty().expect("pty fetch");
+
         let chan = self.door.with_runner(|runner| {
-            runner.open_client_session(exec, false)
+            runner.open_client_session(exec, Some(pty))
         }).await?;
 
         let cstd = ChanInOut::new(chan, &self.door);
diff --git a/async/src/lib.rs b/async/src/lib.rs
index ee0f84b24eced466b753b69ee4806789f3b86791..8c167c6258a62035fc39a803050f287a3765dfea 100644
--- a/async/src/lib.rs
+++ b/async/src/lib.rs
@@ -1,9 +1,9 @@
-#![forbid(unsafe_code)]
 #![allow(unused_imports)]
 
 mod client;
 mod async_door;
 mod cmdline_client;
+mod pty;
 
 pub use async_door::AsyncDoor;
 pub use client::SSHClient;
@@ -13,3 +13,5 @@ pub use cmdline_client::CmdlineClient;
 mod fdio;
 #[cfg(unix)]
 pub use fdio::{stdin, stdout, stderr};
+
+pub use pty::raw_pty;
diff --git a/sshproto/src/channel.rs b/sshproto/src/channel.rs
index 1144b8e65c2695cfa8549b51535fc3ef6895f73c..8e50793776a6b143ee337f44f82a8ed3ac6a8fbf 100644
--- a/sshproto/src/channel.rs
+++ b/sshproto/src/channel.rs
@@ -11,7 +11,7 @@ use heapless::{Deque, String, Vec};
 use crate::{conn::RespPackets, *};
 use config::*;
 use packets::{ChannelReqType, ChannelRequest, Packet, ChannelOpenType, ChannelData, ChannelDataExt};
-use sshwire::BinString;
+use sshwire::{BinString, TextString};
 
 pub(crate) struct Channels {
     ch: [Option<Channel>; config::MAX_CHANNELS],
@@ -257,21 +257,21 @@ impl From<&ChannelOpenType<'_>> for ChanType {
 }
 
 #[derive(Debug)]
-struct ModePair {
-    opcode: u8,
-    arg: u32,
+pub struct ModePair {
+    pub opcode: u8,
+    pub arg: u32,
 }
 
 #[derive(Debug)]
 pub struct Pty {
     // or could we put String into packets::Pty and serialize modes there...
-    term: String<MAX_TERM>,
-    cols: u32,
-    rows: u32,
-    width: u32,
-    height: u32,
+    pub term: String<MAX_TERM>,
+    pub cols: u32,
+    pub rows: u32,
+    pub width: u32,
+    pub height: u32,
     // TODO: perhaps we need something serializable here
-    modes: Vec<ModePair, { termmodes::NUM_MODES }>,
+    pub modes: Vec<ModePair, { termmodes::NUM_MODES }>,
 }
 
 pub(crate) type ExecString = heapless::String<MAX_EXEC>;
@@ -310,8 +310,15 @@ impl Req {
         let want_reply = self.details.want_reply();
         let ty = match &self.details {
             ReqDetails::Shell => ChannelReqType::Shell,
-            ReqDetails::Pty(_pty) => {
-                todo!("serialize modes")
+            ReqDetails::Pty(pty) => {
+                ChannelReqType::Pty(packets::Pty {
+                    term: TextString(pty.term.as_bytes()),
+                    cols: pty.cols,
+                    rows: pty.rows,
+                    width: pty.width,
+                    height: pty.height,
+                    modes: BinString(&[]),
+                })
             }
             ReqDetails::Exec(cmd) => {
                 ChannelReqType::Exec(packets::Exec { command: cmd.as_str().into() })
diff --git a/sshproto/src/config.rs b/sshproto/src/config.rs
index 2b783c013a2eb68d01aad805c7b23b9dd3b3637d..03ff3bbc56c4475a88e70a7c096821fad03aa684 100644
--- a/sshproto/src/config.rs
+++ b/sshproto/src/config.rs
@@ -13,3 +13,5 @@ pub const MAX_EXEC: usize = 200;
 // Unsure if this is specified somewhere
 pub const MAX_TERM: usize = 32;
 
+pub const DEFAULT_TERM: &str = "vt220";
+
diff --git a/sshproto/src/lib.rs b/sshproto/src/lib.rs
index 06ce9d46228c933c02641b6bf5b12c23e812763f..c5ff43e07cd1dcb5ee2681bc6ac34be31e8fef77 100644
--- a/sshproto/src/lib.rs
+++ b/sshproto/src/lib.rs
@@ -9,7 +9,6 @@
 // XXX unused_imports only during dev churn
 #![allow(unused_imports)]
 
-pub mod packets;
 // XXX decide what is public
 pub mod conn;
 pub mod encrypt;
@@ -35,14 +34,16 @@ mod servauth;
 pub mod doorlog;
 mod auth;
 mod channel;
-mod config;
 mod runner;
 mod behaviour;
 mod termmodes;
 mod async_behaviour;
 mod block_behaviour;
 mod ssh_chapoly;
+
+pub mod packets;
 pub mod sshwire;
+pub mod config;
 
 // Application API
 pub use behaviour::{Behaviour, BhError, BhResult, ResponseString};
@@ -56,5 +57,5 @@ pub use conn::RespPackets;
 pub use sign::SignKey;
 pub use packets::PubKey;
 pub use error::{Error,Result};
-pub use channel::{ChanMsg, ChanMsgDetails, ChanEvent};
+pub use channel::{ChanMsg, ChanMsgDetails, ChanEvent, Pty};
 pub use conn::Event;
diff --git a/sshproto/src/runner.rs b/sshproto/src/runner.rs
index ae96403c14d5f92e8f399064d600e80d7afc5f53..cff34740881d4a380078e31650375aaa61e73ea2 100644
--- a/sshproto/src/runner.rs
+++ b/sshproto/src/runner.rs
@@ -152,11 +152,12 @@ impl<'a> Runner<'a> {
         }
     }
 
-    pub fn open_client_session(&mut self, exec: Option<&str>, pty: bool) -> Result<u32> {
+    // TODO: move somewhere client specific?
+    pub fn open_client_session(&mut self, exec: Option<&str>, pty: Option<channel::Pty>) -> Result<u32> {
         trace!("open_client_session");
         let mut init_req = channel::InitReqs::new();
-        if pty {
-            todo!("pty needs modes and that");
+        if let Some(pty) = pty {
+            init_req.push(channel::ReqDetails::Pty(pty)).trap()?;
         }
         if let Some(cmd) = exec {
             let mut s = channel::ExecString::new();