diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..fa8d85ac52f19959d6fc9942c265708b4b3c2b04
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+Cargo.lock
+target
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..12a9c27e6617bbb90faa02b15936d1af8aec1b91
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "redox_installer"
+version = "0.1.0"
+
+[[bin]]
+name = "redox_installer"
+path = "src/bin/installer.rs"
+
+[lib]
+name = "redox_installer"
+path = "src/lib.rs"
+
+[dependencies]
+liner = "0.1"
+rand = "0.3"
+serde = "0.8"
+serde_derive = "0.8"
+termion = "1.1"
+toml = { version = "0.2", default-features = false, features = ["serde"] }
+userutils = { git = "https://github.com/redox-os/userutils.git" }
diff --git a/README.md b/README.md
index a24c902e054b60cd600c91f605fc3353918716c0..33f287c4cc2d9f45d7a04c2449d2675285834785 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,55 @@
-# installer
+# Redox OS installer
+
+The Redox installer will allow you to produce a Redox OS image. You will
+be able to specify:
+- Output device (raw image, ISO, QEMU, VirtualBox, drive)
+- Filesystem
+- Included packages
+- Method of installation (from source, from binary)
+- User accounts
+
+You will be prompted to install dependencies, based on your OS and method of
+installation. The easiest method is to install from binaries.
+
+## Usage
+
+It is recommended to compile with `cargo`, in release mode:
+```bash
+cargo build --release
+```
+
+By default, you will be prompted to supply configuration options. You can
+use the scripted mode by supplying a configuration file:
+```bash
+cargo run --release -- config/example.toml
+```
+An example configuration can be found in [config/example.toml](./config/example.toml).
+Unsuplied configuration will use the default. You can use the `general.prompt`
+setting to prompt when configuration is not set. Multiple configurations can
+be specified, they will be built in order.
+
+## Embedding
+
+The installer can also be used inside of other crates, as a library:
+
+```toml
+# Cargo.toml
+[dependencies]
+redox_installer = "0.1"
+```
+
+```rust
+// src/main.rs
+extern crate redox_installer;
+
+use std::io;
+
+fn main() {
+    let stdout = io::stdout();
+    let mut stdout = stdout.lock();
+    
+    let mut config = redox_installer::Config::default();
+    ...
+    redox_installer::install(&mut stdout, &config);
+}
+```
diff --git a/config/example.toml b/config/example.toml
new file mode 100644
index 0000000000000000000000000000000000000000..e8282196f3c4e9c7a9c24171c161d50c87a107ba
--- /dev/null
+++ b/config/example.toml
@@ -0,0 +1,17 @@
+# This is an example configuration file
+
+# General settings
+[general]
+# Prompt if settings are not defined
+prompt = true
+
+# Package settings
+[packages]
+orbutils = {}
+
+# User settings
+[users.root]
+uid = 0
+gid = 0
+
+[users.user]
diff --git a/src/bin/installer.rs b/src/bin/installer.rs
new file mode 100644
index 0000000000000000000000000000000000000000..10fc634626884b2d229b920f98282d377fb9395d
--- /dev/null
+++ b/src/bin/installer.rs
@@ -0,0 +1,65 @@
+extern crate redox_installer;
+extern crate serde;
+extern crate toml;
+
+use std::{env, process};
+use std::fs::File;
+use std::io::{self, Read, Write};
+
+fn main() {
+    let stderr = io::stderr();
+    let mut stderr = stderr.lock();
+
+    let mut configs = vec![];
+    for arg in env::args().skip(1) {
+        match File::open(&arg) {
+            Ok(mut config_file) => {
+                let mut config_data = String::new();
+                match config_file.read_to_string(&mut config_data) {
+                    Ok(_) => {
+                        let mut parser = toml::Parser::new(&config_data);
+                        match parser.parse() {
+                            Some(parsed) => {
+                                let mut decoder = toml::Decoder::new(toml::Value::Table(parsed));
+                                match serde::Deserialize::deserialize(&mut decoder) {
+                                    Ok(config) => {
+                                        configs.push(config);
+                                    },
+                                    Err(err) => {
+                                        writeln!(stderr, "installer: {}: failed to decode: {}", arg, err).unwrap();
+                                        process::exit(1);
+                                    }
+                                }
+                            },
+                            None => {
+                                for error in parser.errors {
+                                    writeln!(stderr, "installer: {}: failed to parse: {}", arg, error).unwrap();
+                                }
+                                process::exit(1);
+                            }
+                        }
+                    },
+                    Err(err) => {
+                        writeln!(stderr, "installer: {}: failed to read: {}", arg, err).unwrap();
+                        process::exit(1);
+                    }
+                }
+            },
+            Err(err) => {
+                writeln!(stderr, "installer: {}: failed to open: {}", arg, err).unwrap();
+                process::exit(1);
+            }
+        }
+    }
+
+    if configs.is_empty() {
+        configs.push(redox_installer::Config::default());
+    }
+
+    for config in configs {
+        if let Err(err) = redox_installer::install(config) {
+            writeln!(stderr, "installer: failed to install: {}", err).unwrap();
+            process::exit(1);
+        }
+    }
+}
diff --git a/src/config/general.rs b/src/config/general.rs
new file mode 100644
index 0000000000000000000000000000000000000000..f247293663df80547830aa2dd7c43582c61f1aeb
--- /dev/null
+++ b/src/config/general.rs
@@ -0,0 +1,4 @@
+#[derive(Debug, Default, Deserialize)]
+pub struct GeneralConfig {
+    pub prompt: bool
+}
diff --git a/src/config/mod.rs b/src/config/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d9cee066e5b5e66d177514ba72759eab3bb42d03
--- /dev/null
+++ b/src/config/mod.rs
@@ -0,0 +1,12 @@
+use std::collections::BTreeMap;
+
+mod general;
+mod package;
+mod user;
+
+#[derive(Debug, Default, Deserialize)]
+pub struct Config {
+    pub general: general::GeneralConfig,
+    pub packages: BTreeMap<String, package::PackageConfig>,
+    pub users: BTreeMap<String, user::UserConfig>,
+}
diff --git a/src/config/package.rs b/src/config/package.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2a01d77cffd0489cabc6c12125b0e9060bf2d834
--- /dev/null
+++ b/src/config/package.rs
@@ -0,0 +1,6 @@
+#[derive(Debug, Default, Deserialize)]
+pub struct PackageConfig {
+    pub version: String,
+    pub git: String,
+    pub path: String,
+}
diff --git a/src/config/user.rs b/src/config/user.rs
new file mode 100644
index 0000000000000000000000000000000000000000..96113c68faf92fcd6871116d49a89ef2a7e6e7d9
--- /dev/null
+++ b/src/config/user.rs
@@ -0,0 +1,9 @@
+#[derive(Debug, Default, Deserialize)]
+pub struct UserConfig {
+    pub password: Option<String>,
+    pub uid: Option<u32>,
+    pub gid: Option<u32>,
+    pub name: Option<String>,
+    pub home: Option<String>,
+    pub shell: Option<String>,
+}
diff --git a/src/install.rs b/src/install.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d17fce43781b15c892e08aa2b363e0d5accb30d8
--- /dev/null
+++ b/src/install.rs
@@ -0,0 +1,126 @@
+extern crate liner;
+extern crate rand;
+extern crate termion;
+extern crate userutils;
+
+use self::rand::Rng;
+use self::termion::input::TermRead;
+
+use std::io::{self, Write};
+use std::str::FromStr;
+
+use config::Config;
+
+fn unwrap_or_prompt<T: FromStr>(option: Option<T>, context: &mut liner::Context, prompt: &str) -> Result<T, String> {
+    match option {
+        None => {
+            let line = context.read_line(prompt, &mut |_| {}).map_err(|err| format!("failed to read line: {}", err))?;
+            T::from_str(&line).map_err(|_| format!("failed to parse '{}'", line))
+        },
+        Some(t) => Ok(t)
+    }
+}
+
+fn prompt_password(prompt: &str, confirm_prompt: &str) -> Result<String, String> {
+    let stdin = io::stdin();
+    let mut stdin = stdin.lock();
+    let stdout = io::stdout();
+    let mut stdout = stdout.lock();
+
+    stdout.write(prompt.as_bytes()).map_err(|err| format!("failed to write to stdout: {}", err))?;
+    stdout.flush().map_err(|err| format!("failed to flush stdout: {}", err))?;
+    if let Some(password) = stdin.read_passwd(&mut stdout).map_err(|err| format!("failed to read password: {}", err))? {
+        stdout.write(b"\n").map_err(|err| format!("failed to write to stdout: {}", err))?;
+        stdout.flush().map_err(|err| format!("failed to flush stdout: {}", err))?;
+
+        if password.is_empty() {
+            Ok(password)
+        } else {
+            stdout.write(confirm_prompt.as_bytes()).map_err(|err| format!("failed to write to stdout: {}", err))?;
+            stdout.flush().map_err(|err| format!("failed to flush stdout: {}", err))?;
+            if let Some(confirm_password) = stdin.read_passwd(&mut stdout).map_err(|err| format!("failed to read password: {}", err))? {
+                stdout.write(b"\n").map_err(|err| format!("failed to write to stdout: {}", err))?;
+                stdout.flush().map_err(|err| format!("failed to flush stdout: {}", err))?;
+
+                if confirm_password == password {
+                    let salt = format!("{:X}", rand::OsRng::new().unwrap().next_u64());
+                    Ok(userutils::Passwd::encode(&password, &salt))
+                } else {
+                    Err("passwords do not match".to_string())
+                }
+            } else {
+                Err("passwords do not match".to_string())
+            }
+        }
+    } else {
+        Ok(String::new())
+    }
+}
+
+pub fn install(config: Config) -> Result<(), String> {
+    println!("Install {:#?}", config);
+
+    let mut context = liner::Context::new();
+
+    macro_rules! prompt {
+        ($dst:expr, $($arg:tt)*) => (if config.general.prompt {
+            unwrap_or_prompt($dst, &mut context, &format!($($arg)*))
+        } else {
+            Ok($dst.unwrap_or_default())
+        })
+    }
+
+    macro_rules! prompt_default {
+        ($dst:expr, $def:expr, $($arg:tt)*) => (if config.general.prompt {
+            match unwrap_or_prompt($dst, &mut context, &format!($($arg)*)) {
+                Ok(res) => if res.is_empty() {
+                    Ok($def)
+                } else {
+                    Ok(res)
+                },
+                Err(err) => Err(err)
+            }
+        } else {
+            Ok($def)
+        })
+    }
+
+    let mut passwd = String::new();
+
+    let mut next_uid = 1000;
+    for (username, user) in config.users {
+        let password = if let Some(password) = user.password {
+            password
+        } else if config.general.prompt {
+            prompt_password(&format!("{}: enter password: ", username), &format!("{}: confirm password: ", username))?
+        } else {
+            String::new()
+        };
+
+        let uid = user.uid.unwrap_or(next_uid);
+
+        if uid >= next_uid {
+            next_uid = uid + 1;
+        }
+
+        let gid = user.gid.unwrap_or(uid);
+
+        let name = prompt_default!(user.name, username.clone(), "{}: name: ", username)?;
+        let home = prompt_default!(user.home, format!("/home/{}", username), "{}: home: ", username)?;
+        let shell = prompt_default!(user.shell, "/bin/ion".to_string(), "{}: shell: ", username)?;
+
+        println!("Creating user {}:", username);
+        println!("\tPassword: {}", password);
+        println!("\tUID: {}", uid);
+        println!("\tGID: {}", gid);
+        println!("\tName: {}", name);
+        println!("\tHome: {}", home);
+        println!("\tShell: {}", shell);
+
+        passwd.push_str(&format!("{};{};{};{};{};{};{}\n", username, password, uid, gid, name, home, shell));
+    }
+
+    print!("/etc/passwd:\n{}", passwd);
+
+    Ok(())
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2d6a50608d0c17904d5193dbedf3112b71bc155e
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,8 @@
+#[macro_use]
+extern crate serde_derive;
+
+pub use config::Config;
+pub use install::install;
+
+mod config;
+mod install;