diff --git a/gulpfile.ts b/gulpfile.ts
index bdc20089cdab5efb8eeb1ff473a60df9bac22094..b394e4f44cfce47c0736aa398d4ce247d2627d8e 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -45,7 +45,7 @@ gulp.task('build:copy:locales', cb => {
 });
 
 gulp.task('build:client:script', () => {
-	return gulp.src(['./src/server/web/boot.js'])
+	return gulp.src(['./src/server/web/boot.js', './src/server/web/bios.js', './src/server/web/cli.js'])
 		.pipe(replace('VERSION', JSON.stringify(meta.version)))
 		.pipe(replace('LANGS', JSON.stringify(Object.keys(locales))))
 		.pipe(terser({
@@ -55,7 +55,7 @@ gulp.task('build:client:script', () => {
 });
 
 gulp.task('build:client:style', () => {
-	return gulp.src(['./src/server/web/style.css'])
+	return gulp.src(['./src/server/web/style.css', './src/server/web/bios.css', './src/server/web/cli.css'])
 		.pipe(cssnano())
 		.pipe(gulp.dest('./built/server/web/'));
 });
diff --git a/src/client/components/form/link.vue b/src/client/components/form/link.vue
index 7093f50397f51c98626abfac5f8ba52a8a1d9c7e..2efc6b58c9cf81c9861c66b494e1f4cf305f2436 100644
--- a/src/client/components/form/link.vue
+++ b/src/client/components/form/link.vue
@@ -8,7 +8,7 @@
 			<Fa :icon="faExternalLinkAlt" class="icon"/>
 		</span>
 	</a>
-	<MkA class="main _button _formPanel _formClickable" :class="{ active }" :to="to" v-else>
+	<MkA class="main _button _formPanel _formClickable" :class="{ active }" :to="to" :behavior="behavior" v-else>
 		<span class="icon"><slot name="icon"></slot></span>
 		<span class="text"><slot></slot></span>
 		<span class="right">
@@ -38,6 +38,10 @@ export default defineComponent({
 			type: Boolean,
 			required: false
 		},
+		behavior: {
+			type: String,
+			required: false,
+		},
 	},
 	data() {
 		return {
diff --git a/src/client/components/global/a.vue b/src/client/components/global/a.vue
index cf894deaba042645ebaf94d562370471bebaa401..d293cb571f8706b3e53dd41205a889626d57230d 100644
--- a/src/client/components/global/a.vue
+++ b/src/client/components/global/a.vue
@@ -98,6 +98,11 @@ export default defineComponent({
 		},
 
 		nav() {
+			if (this.behavior === 'browser') {
+				location.href = this.to;
+				return;
+			}
+
 			if (this.to.startsWith('/my/messaging')) {
 				if (ColdDeviceStorage.get('chatOpenBehavior') === 'window') return this.window();
 				if (ColdDeviceStorage.get('chatOpenBehavior') === 'popout') return this.popout();
diff --git a/src/client/pages/settings/other.vue b/src/client/pages/settings/other.vue
index c0b9625098c7a844bed6b88099c9702cb1d1b26d..a14e101328dbb72ca4a600719b4e52258afc6977 100644
--- a/src/client/pages/settings/other.vue
+++ b/src/client/pages/settings/other.vue
@@ -23,13 +23,16 @@
 
 	<FormLink to="/settings/registry"><template #icon><Fa :icon="faCogs"/></template>{{ $ts.registry }}</FormLink>
 
+	<FormLink to="/bios" behavior="browser"><template #icon><Fa :icon="faDoorOpen"/></template>BIOS</FormLink>
+	<FormLink to="/cli" behavior="browser"><template #icon><Fa :icon="faDoorOpen"/></template>CLI</FormLink>
+
 	<FormButton @click="closeAccount" danger>{{ $ts.closeAccount }}</FormButton>
 </FormBase>
 </template>
 
 <script lang="ts">
 import { defineAsyncComponent, defineComponent } from 'vue';
-import { faEllipsisH, faCogs } from '@fortawesome/free-solid-svg-icons';
+import { faEllipsisH, faCogs, faDoorOpen } from '@fortawesome/free-solid-svg-icons';
 import FormSwitch from '@/components/form/switch.vue';
 import FormSelect from '@/components/form/select.vue';
 import FormLink from '@/components/form/link.vue';
@@ -61,7 +64,7 @@ export default defineComponent({
 				icon: faEllipsisH
 			},
 			debug,
-			faCogs
+			faCogs, faDoorOpen,
 		}
 	},
 
diff --git a/src/server/web/bios.css b/src/server/web/bios.css
new file mode 100644
index 0000000000000000000000000000000000000000..b0da3ee39b1c1a23ab0244c3ea57e3e8f43b8aa1
--- /dev/null
+++ b/src/server/web/bios.css
@@ -0,0 +1,40 @@
+* {
+	font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
+}
+
+html {
+	background: #ffb4e1;
+}
+
+main {
+	background: #dedede;
+}
+main > .tabs {
+	padding: 16px;
+	border-bottom: solid 4px #c3c3c3;
+}
+
+#lsEditor > .adder {
+	margin: 16px;
+	padding: 16px;
+	border: solid 2px #c3c3c3;
+}
+#lsEditor > .adder > textarea {
+	display: block;
+	width: 100%;
+	min-height: 5em;
+	box-sizing: border-box;
+}
+#lsEditor > .record {
+	padding: 16px;
+	border-bottom: solid 1px #c3c3c3;
+}
+#lsEditor > .record > header {
+	font-weight: bold;
+}
+#lsEditor > .record > textarea {
+	display: block;
+	width: 100%;
+	min-height: 5em;
+	box-sizing: border-box;
+}
diff --git a/src/server/web/bios.js b/src/server/web/bios.js
new file mode 100644
index 0000000000000000000000000000000000000000..d06dee801aea3c6ba254da81acde5ac1f1ee05fa
--- /dev/null
+++ b/src/server/web/bios.js
@@ -0,0 +1,87 @@
+'use strict';
+
+window.onload = async () => {
+	const account = JSON.parse(localStorage.getItem('account'));
+	const i = account.token;
+
+	const api = (endpoint, data = {}) => {
+		const promise = new Promise((resolve, reject) => {
+			// Append a credential
+			if (i) data.i = i;
+	
+			// Send request
+			fetch(endpoint.indexOf('://') > -1 ? endpoint : `/api/${endpoint}`, {
+				method: 'POST',
+				body: JSON.stringify(data),
+				credentials: 'omit',
+				cache: 'no-cache'
+			}).then(async (res) => {
+				const body = res.status === 204 ? null : await res.json();
+	
+				if (res.status === 200) {
+					resolve(body);
+				} else if (res.status === 204) {
+					resolve();
+				} else {
+					reject(body.error);
+				}
+			}).catch(reject);
+		});
+		
+		return promise;
+	};
+
+	const content = document.getElementById('content');
+
+	document.getElementById('ls').addEventListener('click', () => {
+		content.innerHTML = '';
+
+		const lsEditor = document.createElement('div');
+		lsEditor.id = 'lsEditor';
+
+		const adder = document.createElement('div');
+		adder.classList.add('adder');
+		const addKeyInput = document.createElement('input');
+		const addValueTextarea = document.createElement('textarea');
+		const addButton = document.createElement('button');
+		addButton.textContent = 'add';
+		addButton.addEventListener('click', () => {
+			localStorage.setItem(addKeyInput.value, addValueTextarea.value);
+			location.reload();
+		});
+
+		adder.appendChild(addKeyInput);
+		adder.appendChild(addValueTextarea);
+		adder.appendChild(addButton);
+		lsEditor.appendChild(adder);
+
+		for (let i = 0; i < localStorage.length; i++) {
+			const k = localStorage.key(i);
+			const record = document.createElement('div');
+			record.classList.add('record');
+			const header = document.createElement('header');
+			header.textContent = k;
+			const textarea = document.createElement('textarea');
+			textarea.textContent = localStorage.getItem(k);
+			const saveButton = document.createElement('button');
+			saveButton.textContent = 'save';
+			saveButton.addEventListener('click', () => {
+				localStorage.setItem(k, textarea.value);
+				location.reload();
+			});
+			const removeButton = document.createElement('button');
+			removeButton.textContent = 'remove';
+			removeButton.addEventListener('click', () => {
+				localStorage.removeItem(k);
+				location.reload();
+			});
+			record.appendChild(header);
+			record.appendChild(textarea);
+			record.appendChild(saveButton);
+			record.appendChild(removeButton);
+			lsEditor.appendChild(record);
+		}
+
+		content.appendChild(lsEditor);
+	});
+};
diff --git a/src/server/web/cli.css b/src/server/web/cli.css
new file mode 100644
index 0000000000000000000000000000000000000000..07cd27830b692b0824a57c54a0feb33ba91ed097
--- /dev/null
+++ b/src/server/web/cli.css
@@ -0,0 +1,19 @@
+* {
+	font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
+}
+
+html {
+	background: #ffb4e1;
+}
+
+main {
+	background: #dedede;
+}
+
+#tl > div {
+	padding: 16px;
+	border-bottom: solid 1px #c3c3c3;
+}
+#tl > div > header {
+	font-weight: bold;
+}
diff --git a/src/server/web/cli.js b/src/server/web/cli.js
new file mode 100644
index 0000000000000000000000000000000000000000..3dff1d4860431a305e7c1857c62e8816a94275d4
--- /dev/null
+++ b/src/server/web/cli.js
@@ -0,0 +1,55 @@
+'use strict';
+
+window.onload = async () => {
+	const account = JSON.parse(localStorage.getItem('account'));
+	const i = account.token;
+
+	const api = (endpoint, data = {}) => {
+		const promise = new Promise((resolve, reject) => {
+			// Append a credential
+			if (i) data.i = i;
+	
+			// Send request
+			fetch(endpoint.indexOf('://') > -1 ? endpoint : `/api/${endpoint}`, {
+				method: 'POST',
+				body: JSON.stringify(data),
+				credentials: 'omit',
+				cache: 'no-cache'
+			}).then(async (res) => {
+				const body = res.status === 204 ? null : await res.json();
+	
+				if (res.status === 200) {
+					resolve(body);
+				} else if (res.status === 204) {
+					resolve();
+				} else {
+					reject(body.error);
+				}
+			}).catch(reject);
+		});
+		
+		return promise;
+	};
+
+	document.getElementById('submit').addEventListener('click', () => {
+		api('notes/create', {
+			text: document.getElementById('text').value
+		}).then(() => {
+			location.reload();
+		});
+	});
+
+	api('notes/timeline').then(notes => {
+		const tl = document.getElementById('tl');
+		for (const note of notes) {
+			const el = document.createElement('div');
+			const name = document.createElement('header');
+			name.textContent = `${note.user.name} @${note.user.username}`;
+			const text = document.createElement('div');
+			text.textContent = `${note.text}`;
+			el.appendChild(name);
+			el.appendChild(text);
+			tl.appendChild(el);
+		}
+	});
+};
diff --git a/src/server/web/index.ts b/src/server/web/index.ts
index 8ea7e157519bd8d509d24d7598ef5cf2993cb422..a1d79100a6ddefe4f58ca4de087fbc58b1f35f69 100644
--- a/src/server/web/index.ts
+++ b/src/server/web/index.ts
@@ -376,6 +376,18 @@ router.get('/info', async ctx => {
 	});
 });
 
+router.get('/bios', async ctx => {
+	await ctx.render('bios', {
+		version: config.version,
+	});
+});
+
+router.get('/cli', async ctx => {
+	await ctx.render('cli', {
+		version: config.version,
+	});
+});
+
 const override = (source: string, target: string, depth: number = 0) =>
 	[, ...target.split('/').filter(x => x), ...source.split('/').filter(x => x).splice(depth)].join('/');
 
diff --git a/src/server/web/views/bios.pug b/src/server/web/views/bios.pug
new file mode 100644
index 0000000000000000000000000000000000000000..d81a3ee67fb439792f7de55cf720021450183e5f
--- /dev/null
+++ b/src/server/web/views/bios.pug
@@ -0,0 +1,20 @@
+doctype html
+
+html
+
+	head
+		meta(charset='utf-8')
+		meta(name='application-name' content='Misskey')
+		title Misskey BIOS
+		style
+			include ../bios.css
+		script
+			include ../bios.js
+
+	body
+		header
+			h1 Misskey BIOS #{version}
+		main
+			div.tabs
+				button#ls edit local storage
+			div#content
diff --git a/src/server/web/views/cli.pug b/src/server/web/views/cli.pug
new file mode 100644
index 0000000000000000000000000000000000000000..d2cf7c4335dff40b2ad05a369695bad5ad9b9515
--- /dev/null
+++ b/src/server/web/views/cli.pug
@@ -0,0 +1,21 @@
+doctype html
+
+html
+
+	head
+		meta(charset='utf-8')
+		meta(name='application-name' content='Misskey')
+		title Misskey Cli
+		style
+			include ../cli.css
+		script
+			include ../cli.js
+
+	body
+		header
+			h1 Misskey Cli #{version}
+		main
+			div#form
+				textarea#text
+				button#submit submit
+			div#tl