diff --git a/src/sbbs3/js_console.cpp b/src/sbbs3/js_console.cpp
index ea0bd023f232da6088978fb314ce952e15f10a05..3610ff8fd5ad7709946c561b3ca0d5cdc090eef5 100644
--- a/src/sbbs3/js_console.cpp
+++ b/src/sbbs3/js_console.cpp
@@ -271,37 +271,10 @@ static JSBool js_console_set(JSContext *cx, JSObject *obj, jsid id, JSBool stric
 			break;
 		case CON_PROP_CTRLKEY_PASSTHRU:
 			if(JSVAL_IS_STRING(*vp)) {
-				/* op can be 0 for replace, + for add, or - for remove */
-				int op=0;
 				char *s;
-				char ctrl;
 
-				if((str=JS_ValueToString(cx, *vp))==NULL)
-					break;
-				val=sbbs->cfg.ctrlkey_passthru;
 				JSSTRING_TO_STRING(cx, str, s, NULL);
-				for(; *s; s++) {
-					if(*s=='+')
-						op=1;
-					else if(*s=='-')
-						op=2;
-					else {
-						if(!op) {
-							val=0;
-							op=1;
-						}
-						ctrl=toupper(*s);
-						ctrl&=0x1f;			/* Ensure it fits */
-						switch(op) {
-							case 1:		/* Add to the set */
-								val |= 1<<ctrl;
-								break;
-							case 2:		/* Remove from the set */
-								val &= ~(1<<ctrl);
-								break;
-						}
-					}
-				}
+				val=str_to_bits(sbbs->cfg.ctrlkey_passthru, s);
 			}
 			sbbs->cfg.ctrlkey_passthru=val;
 			break;
diff --git a/src/sbbs3/js_user.c b/src/sbbs3/js_user.c
index 9fddde0ca3baad919cf4b2152ffb67fec6716c93..f88fa302949c591665f512a017f4c682e8728555 100644
--- a/src/sbbs3/js_user.c
+++ b/src/sbbs3/js_user.c
@@ -583,43 +583,73 @@ static JSBool js_user_set(JSContext *cx, JSObject *obj, jsid id, JSBool strict,
 			break;
 		case USER_PROP_FLAGS1:
 			JS_RESUMEREQUEST(cx, rc);
-			if(!JS_ValueToInt32(cx,*vp,&val))
-				return JS_FALSE;
+			if(JSVAL_IS_STRING(*vp)) {
+				val=str_to_bits(p->user->flags1, str);
+			}
+			else {
+				if(!JS_ValueToInt32(cx,*vp,&val))
+					return JS_FALSE;
+			}
 			putuserrec(p->cfg,p->user->number,U_FLAGS1,0,ultoa(p->user->flags1=val,tmp,16));
 			rc=JS_SUSPENDREQUEST(cx);
 			break;
 		case USER_PROP_FLAGS2:
 			JS_RESUMEREQUEST(cx, rc);
-			if(!JS_ValueToInt32(cx,*vp,&val))
-				return JS_FALSE;
+			if(JSVAL_IS_STRING(*vp)) {
+				val=str_to_bits(p->user->flags1, str);
+			}
+			else {
+				if(!JS_ValueToInt32(cx,*vp,&val))
+					return JS_FALSE;
+			}
 			putuserrec(p->cfg,p->user->number,U_FLAGS2,0,ultoa(p->user->flags2=val,tmp,16));
 			rc=JS_SUSPENDREQUEST(cx);
 			break;
 		case USER_PROP_FLAGS3:
 			JS_RESUMEREQUEST(cx, rc);
-			if(!JS_ValueToInt32(cx,*vp,&val))
-				return JS_FALSE;
+			if(JSVAL_IS_STRING(*vp)) {
+				val=str_to_bits(p->user->flags1, str);
+			}
+			else {
+				if(!JS_ValueToInt32(cx,*vp,&val))
+					return JS_FALSE;
+			}
 			putuserrec(p->cfg,p->user->number,U_FLAGS3,0,ultoa(p->user->flags3=val,tmp,16));
 			rc=JS_SUSPENDREQUEST(cx);
 			break;
 		case USER_PROP_FLAGS4:
 			JS_RESUMEREQUEST(cx, rc);
-			if(!JS_ValueToInt32(cx,*vp,&val))
-				return JS_FALSE;
+			if(JSVAL_IS_STRING(*vp)) {
+				val=str_to_bits(p->user->flags1, str);
+			}
+			else {
+				if(!JS_ValueToInt32(cx,*vp,&val))
+					return JS_FALSE;
+			}
 			putuserrec(p->cfg,p->user->number,U_FLAGS4,0,ultoa(p->user->flags4=val,tmp,16));
 			rc=JS_SUSPENDREQUEST(cx);
 			break;
 		case USER_PROP_EXEMPT:
 			JS_RESUMEREQUEST(cx, rc);
-			if(!JS_ValueToInt32(cx,*vp,&val))
-				return JS_FALSE;
+			if(JSVAL_IS_STRING(*vp)) {
+				val=str_to_bits(p->user->flags1, str);
+			}
+			else {
+				if(!JS_ValueToInt32(cx,*vp,&val))
+					return JS_FALSE;
+			}
 			putuserrec(p->cfg,p->user->number,U_EXEMPT,0,ultoa(p->user->exempt=val,tmp,16));
 			rc=JS_SUSPENDREQUEST(cx);
 			break;
 		case USER_PROP_REST:	
 			JS_RESUMEREQUEST(cx, rc);
-			if(!JS_ValueToInt32(cx,*vp,&val))
-				return JS_FALSE;
+			if(JSVAL_IS_STRING(*vp)) {
+				val=str_to_bits(p->user->flags1, str);
+			}
+			else {
+				if(!JS_ValueToInt32(cx,*vp,&val))
+					return JS_FALSE;
+			}
 			putuserrec(p->cfg,p->user->number,U_REST,0,ultoa(p->user->rest=val,tmp,16));
 			rc=JS_SUSPENDREQUEST(cx);
 			break;
@@ -774,12 +804,12 @@ static char* user_security_prop_desc[] = {
 	 "password"
 	,"date password last modified (time_t format)"
 	,"security level (0-99)"
-	,"flag set #1 (bitfield)"
-	,"flag set #2 (bitfield)"
-	,"flag set #3 (bitfield)"
-	,"flag set #4 (bitfield)"
-	,"exemption flags (bitfield)"
-	,"restriction flags (bitfield)"
+	,"flag set #1 (bitfield) can use +/-[A-?] notation"
+	,"flag set #2 (bitfield) can use +/-[A-?] notation"
+	,"flag set #3 (bitfield) can use +/-[A-?] notation"
+	,"flag set #4 (bitfield) can use +/-[A-?] notation"
+	,"exemption flags (bitfield) can use +/-[A-?] notation"
+	,"restriction flags (bitfield) can use +/-[A-?] notation"
 	,"credits"
 	,"free credits (for today only)"
 	,"extra minutes (time bank)"
diff --git a/src/sbbs3/sbbs.h b/src/sbbs3/sbbs.h
index f3a36af41c3406983dc02f772ae9137f7e1f8ba8..1c6e28b6dbe3d44a33c645b13688f14dcaa121c8 100644
--- a/src/sbbs3/sbbs.h
+++ b/src/sbbs3/sbbs.h
@@ -921,6 +921,7 @@ extern "C" {
 	DLLEXPORT size_t	DLLCALL strip_invalid_attr(char *str);
 	DLLEXPORT char *	DLLCALL ultoac(ulong l,char *str);
 	DLLEXPORT char *	DLLCALL rot13(char* str);
+	DLLEXPORT uint32_t	DLLCALL str_to_bits(uint32_t currval, const char *str);
 
 	/* msg_id.c */
 	DLLEXPORT char *	DLLCALL ftn_msgid(sub_t* sub, smbmsg_t* msg, char* msgid, size_t);
diff --git a/src/sbbs3/str_util.c b/src/sbbs3/str_util.c
index 7e7de6bf0cb5b78de01c615d3019811b2f36e12e..4e922c44d002b20cccd006ca2ef8dba0efeee0b6 100644
--- a/src/sbbs3/str_util.c
+++ b/src/sbbs3/str_util.c
@@ -699,6 +699,36 @@ char* replace_keyed_values(const char* src
 	return(buf);
 }
 
+uint32_t DLLCALL str_to_bits(uint32_t val, const char *str)
+{
+	/* op can be 0 for replace, + for add, or - for remove */
+	int op=0;
+	char *s;
+	char ctrl;
+
+	for(s=str; *s; s++) {
+		if(*s=='+')
+			op=1;
+		else if(*s=='-')
+			op=2;
+		else {
+			if(!op) {
+				val=0;
+				op=1;
+			}
+			ctrl=toupper(*s);
+			ctrl&=0x1f;			/* Ensure it fits */
+			switch(op) {
+				case 1:		/* Add to the set */
+					val |= 1<<ctrl;
+					break;
+				case 2:		/* Remove from the set */
+					val &= ~(1<<ctrl);
+					break;
+			}
+		}
+	}
+}
 
 #if 0	/* replace_*_values test */